QA

Sprawdzanie treści pobranego pliku pdf. Selenium WebDriver, C# oraz iTextSharp

W dzisiejszym wpisie chcę przedstawić Wam sposób, którego użyłem dla jednego z klientów. Wyzwaniem było pobranie pdfa za pomocą Selenium WebDriver i parsowanie treści do tekstu. Chcieliśmy sprawdzić, czy treść pdfa zawiera to, czego spodziewaliśmy się.


Michał Ślęzak. Programista testów w firmie ArtistGrowth. Jak również konsultant i szkoleniowiec. Szuka miejsc, gdzie automatyzacja może pomóc. Lubi prowadzić prezentacje i warsztaty związane z automatyzacją testów. Prowadzić bloga związanego z automatyzacją testów testingplus.me.


Naszym zadaniem będzie sprawdzenie, czy pdf, który generuje się w naszym systemie, zawiera spodziewaną treść. Dodatkowo sprawdzimy, czy pobiera się zawsze, co da nam większą pewność, że system działa.

Jak będzie wyglądał nasz test?

Nasz test będzie polegał na włączaniu przycisku Pobierz i ściągnięciu treści bezpośrednio do katalogu Downloads, który znajdować się będzie w naszym projekcie. Dlaczego w ten sposób? Ponieważ Chrome automatycznie pobiera ten plik, przez co nie musimy zastanawiać się jak obsłużyć wyskakujący systemowy popup.

Strona, z której będziemy pobierać naszą treść to https://choosemyplate-prod.azureedge.net, na której znalazłem różnego rodzaju pdfy dotyczące np. nawyków żywieniowych. Wejdziemy na stronę https://choosemyplate-prod.azureedge.net/make-small-changes-0, z której pobierzemy pierwszy dokument: MPMW_Tipsheet_7_navigatethebuffet_0

Czego potrzebujemy?

Test, którego celem jest sprawdzenie treści pdfa oraz wcześniej pobranie go. Mój przykład oparłem w C#.

IDE Visual Studio / Rider – korzystamy z .NET 4.7.1

Headless i pdf

Jeżeli chcecie korzystać z ChromeDrivera w trybie headless to pobieranie pdfów nie działa w tym trybie dla C# i ChromeDrivera. Dlaczego akurat headless jest popularny? Zdarza się, że mamy serwer, który nie ma dostępnego UI dla systemu, bo np. nie jest bardzo wydajny. W tej sytuacji mamy możliwość użycia Chrome w trybie headless. Sam headless z Chrome pobiera porównywalną ilość pamięci z okienkowym Chromem. Jednak różnica jest w tym, że możemy mieć słabszą instancję, bo ona sama nie musi mieć graficznej nakładki, która pobiera zasoby serwera.

Jest na to issue na githubie dla niektórych języków są działające sposoby, jednak z tych, co testowałem dla C# dla obecnie najnowszej wersji Chrome nie działają.

Dodanie pdf do Allure

Jest możliwość dodania pdfa również do naszego raportu w Allure. O allure wspominałem na swoim blogu. Zamiast wskazywać ścieżkę do screenshota, możemy wskazać ścieżkę do pdfa w podobny sposób i mieć w raporcie pdf, który został pobrany podczas naszego testu. Allure potrafi dodać wiele plików z różnymi rozszerzeniami, również pdf.

iTextSharp

jest to biblioteka, którą możemy używać w .NET do parsowania treści pdfów na tekst.

Jako przykład wziąłem pdf ze strony związanej z dietą z USA. Pdf nie może być skanem czy plikiem tylko graficznym, sam iTextSharp radzi sobie dość dobrze z parsowaniem tekstu.

XUnit

Popularny test runner dla platformy .NET.

Przechodzimy do kodu

Utworzyłem solucję, w której mam projekt w .NET 4.7.1 typu class library. Zaczynamy od pobrania potrzebnych pakietów NuGetowych.

W tym przypadku będzie to XUnit i z nim powiązane pakiety potrzebne do uruchamiania testów. Kolejną biblioteką, którą dodamy będzie iTextSharp, służący do parsowania treści pobranego pdfa.

Również korzystam z biblioteki Shouldly, która pozwala używać fluent assertions, czyli asercji, przyjemniejszych w odbiorze i często zwracających bardziej dokładny komunikat na temat tego, co poszło nie tak.

Selenium.WebDriver / DotNetSeleniumExtras.WaitHelpers – do korzystania z ExpectedConditions i z Selenium WebDrivera.

Po dodaniu potrzebnych pakietów przechodzimy do dodania folderu z testami „Tests”, gdzie dodajemy klasę TestContentFromPdf, w której zamieścimy docelowy test. Ta klasa będzie dziedziczyła z innej klasy BaseTest, w której dodamy nasz setup drivera i jego dispose. W XUnit robimy to poprzez użycie interfejsu IDisposable dla danej klasy. Dodajemy konstruktor oraz metodę Dispose(). W konstruktorze dodajemy nasz obiekt IWebDrivera, który będzie korzystał z ChromeOptions(). Musimy w tym miejscu dodać opcję, żeby nasz pdf nie otwierał się w przeglądarce i od razu wyświetlał, tylko żeby pobierał się w określonej lokalizacji. Żeby to ustawić korzystamy z poniższej metody:

ChromeOptions

plugins.always_open_pdf_externally — które pozwala zdefiniowanie tego, czy chcemy, żeby nasz pdf otwierał się po pobraniu w przeglądarce, czy tylko pobierał.

download.default_directory– definiujemy, gdzie ma być nasz katalog Downloads.

var chromeOptions = new ChromeOptions();
chromeOptions.AddUserProfilePreference("plugins.always_open_pdf_externally", true);
chromeOptions.AddUserProfilePreference("download.default_directory", TestSettings.PathToDownloads);
driver = new ChromeDriver(chromeOptions);
}

Cała klasa BaseTest.cs

using System;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;

namespace TestContentFromPdf.Extension
{
    public class BaseTest : IDisposable
    {
        protected IWebDriver driver;

        protected BaseTest()
        {
            var chromeOptions = new ChromeOptions();
            chromeOptions.AddUserProfilePreference("plugins.always_open_pdf_externally", true);
            chromeOptions.AddUserProfilePreference("download.default_directory", TestSettings.PathToDownloads);
            driver = new ChromeDriver(chromeOptions);
        }

        public void Dispose()
        {
            driver.Close();
        }
    }
}

Po dodaniu klasy BaseTest dodajemy pusty folder Downloads do naszego projektu.

Następnie dodaję ścieżkę do katalogu, do którego ma pobrać nam się nasz pdf. Stworzyłem klasę pomocniczą, gdzie dodałem właściwość PathToDownloads.

Klasa TestSettings.cs

using System;

namespace TestContentFromPdf.Extension
{
    public class TestSettings
    {
        public static string PathToDownloads => $"{Environment.CurrentDirectory}/Downloads";
    }
}

Po dodaniu klas BaseTest oraz TestSettings przechodzimy do dodania folderu Pages, gdzie umieścimy klasę page objecta będzie w nim nasz selektor dla przycisku, który pobierze plik. Również w tej klasie dodamy metodę DownloadPdf, która będzie właśnie przechodzić do określonego adresu URL i klikać w przycisk do pobrania pliku.

public By FirstAvailableLinkToDocument => By.CssSelector("td a:nth-child(4)");

dodaje ten selector, który jest odpowiednikiem wszystkich przycisków PDF dla „Tip Sheets” na stronie. Będzie klikać w pierwszy element, który ma ten selector. Czyli w naszym przypadku będzie pobrany pierwszy dokument ze strony.

Po dodaniu określonego selectora definiuje pole dla IWebDrivera oraz konstruktor dla klasy HomePage.

Po dodaniu konstruktora przechodzimy do dodania metody, która będzie robić akcje pobrania danego pliku. Definiujemy metodę DownloadPdf, w której wskazujemy ścieżkę do strony z pdfem. Metoda będzie najpierw przechodzić do określonego adresu, następnie kliknąć w pierwsze wystąpienie tego selektora (czyli pierwszy pdf – Tip Sheet)

 public void DownloadPdf()
{
   driver.Navigate().GoToUrl("https://choosemyplate-prod.azureedge.net/make-small-changes-0");
   driver.FindElements(FirstAvailableLinkToDocument).FirstOrDefault()?.Click();
}

Cała klasa HomePage.cs

 public void DownloadPdf()
{
   driver.Navigate().GoToUrl("https://choosemyplate-prod.azureedge.net/make-small-changes-0");
   driver.FindElements(FirstAvailableLinkToDocument).FirstOrDefault()?.Click();
}

Po dodaniu naszego page objectu. Przechodzimy do dodania klasy WaitExtension. Jeżeli korzystałeś czytelniku wcześniej z Selenium WebDriver wiesz, że czekanie na elementy w sposób dynamiczny jest ważne, w kontekście stabilności testów automatycznych UI.

Zawiera dwie metody, pierwszą jest Wait(), która zwraca instancję klasy WebDriverWait(), pozwalającą korzystać z ExpectedConditions() oraz z klasy Unitil. Pozwala ona korzystać z wyrażenia lambda w tym miejscu i możemy czekać na pobranie się naszego określonego pliku.

WaitExtension.cs

using System.Linq;
using OpenQA.Selenium;

namespace TestContentFromPdf.Pages
{
    public class HomePage
    {
        public By FirstAvailableLinkToDocument => By.CssSelector("td a:nth-child(4)");
        
        private IWebDriver driver;
        public HomePage(IWebDriver driver)
        {
            this.driver = driver;
        }
    

        public void DownloadPdf()
        {
            driver.Navigate().GoToUrl("https://choosemyplate-prod.azureedge.net/make-small-changes-0");
            driver.FindElements(FirstAvailableLinkToDocument).FirstOrDefault()?.Click();
        }
    }
}

Przechodzimy do dodania klasy SeleniumExtension, która zawierać będzie metodę umożliwiającą pobranie naszego pdfa.

SeleniumExtension.cs

using System.IO;
using System.Text;
using iTextSharp.text.pdf;
using iTextSharp.text.pdf.parser;
using OpenQA.Selenium;

namespace TestContentFromPdf.Extension
{
    public static class SeleniumExtension
    {
        public static string ReturnTextFromPdf(this IWebDriver driver, string nameFile)
        {
            var pathToFile = $"{TestSettings.PathToDownloads}/{nameFile}.pdf";
            driver.Wait().Until(x => File.Exists(pathToFile));
            var fileInfo = new FileInfo(pathToFile);
            if (fileInfo.Length == 0) return null;
            var result = new StringBuilder();
            using (var reader = new PdfReader(pathToFile))
            {
                for (var page = 1; page <= reader.NumberOfPages; page++)
                {
                    var strategy =
                        new SimpleTextExtractionStrategy();

                    var pageText =
                        PdfTextExtractor.GetTextFromPage(reader, page, strategy);


                    result.Append(pageText);
                }
            }

            return result.ToString();
        }
    }
}

Metoda ReturnTextFromPdf jest extension method dla obiektu drivera. Drugim parametrem jest nazwa pliku do naszego pobranego pdfa.

W pierwszej linii tej metody przypisujemy ścieżkę w zmiennej do naszego pobranego pdfa. Następnie czekamy explicit waitem na to, że plik już istnieje w tej ścieżce na dysku.

Kolejnym krokiem jest użycie klasy FileInfo, która pozwala dla określonej ścieżki sprawdzić, czy plik ma więcej niż 0 kb. W tym przypadku jeżeli ma 0 kb to zwracamy wartość null.

Następnie tworzymy obiekt StringBuildera, w którym umieszczać będzie treść naszego sparsowanego tekstu. Robimy to poprzez użycie obiektu klasy PdfReader z biblioteki iTextSharp. Korzystamy z pętli For i pobieramy treść naszego dokumentu.

W pętli tworzymy obiekt Strategy, który również korzysta z wcześniej wspomnianej biblioteki. Dodajemy go jako parametr do metody GetTextFromPage, gdzie wskazujemy jako parametr obiekt reader ze wskazanym naszym plikiem, page jako numer strony z dokumentu. I jako ostatnie korzystamy ze strategii. SimpleTextExtractionStrategy jest najbardziej podstawową metodą.

Mamy również kilka innych np. LocationTextExtractionStrategy.

Wracamy do naszej klasy TestContentFromPdf, gdzie umieścimy nasz test.
Dziedziczy on z klasy BaseTest, żebyśmy nie musieli za każdym razem setupować i robić dispose na obiekcie IWebDriver.

using Shouldly;
using TestContentFromPdf.Extension;
using TestContentFromPdf.Pages;
using Xunit;

namespace TestContentFromPdf.Tests
{
    public class TestContentFromPdf : BaseTest
    {
        [Fact]
        public void GoToSiteWithMakeSmallChangesPdf_WhenIDownloadIt_CheckContainingOfDefineTextInPdf()
        {
            var homePage = new HomePage(driver);
            homePage.DownloadPdf();
            var returnTextFromPdf = driver.ReturnTextFromPdf("MPMW_Tipsheet_7_navigatethebuffet_0").Replace("n", "");
            var text =
                "Take a lap around the buffet before you start to fill up your plate. Plan ahead so you know what to choose and what to limit.";
            returnTextFromPdf.Contains(text).ShouldBeTrue();
        }
    }
}

Korzystamy z [Fact], który określa dla XUnita, że wybrana metoda jest testem. Dodajemy nazwę dla testu. W teście zaczynamy od inicjalizacji obiektu page objecta, w którym mamy metodę DownloadPdf, wchodzącą na adres. Szuka w nim elementu z linkiem, z którego pobieramy nasz dokument. Następnie pobieramy tekst z pliku o nazwie, którą przekazujemy poprzez parametr. Używamy replace żeby znak nowej linii („n”) zamienić na brak, co ułatwi nam sprawdzenie, czy tekst, który mamy w zmiennej text znajduje się w zmiennej returnTextFromPdf.

Po dodaniu zmiennej returnTextfromPdf przechodzimy do dodania zmiennej text, która zawiera tekst z pdfa. Celem jest sprawdzenie, czy za każdym razem konkretna treść jest zawarta w pdfie. Następnie dodajemy asercje i ShouldBeTrue() pozwala nam sprawdzić, czy zmienna ma wartość True.

Uruchamiamy test

Uruchamiamy nasz test, jak widzicie zakończył się sukcesem.

Jakich pdfów odradzam?

Jeżeli nasz pdf jest zdjęciem jakiegoś tekstu, to iTextSharp nie poradzi z nim sobie. Najlepiej radzi sobie z plikami, gdzie nie ma dużej ilości elementów graficznych, jednak jak widzimy nasz dokument nie jest tylko tekstem, a iTextSharp sobie radzi.

Kod Github

Cały przykład znajduje się na githubie.

Podsumowanie

W dzisiejszym wpisie dowiedzieliśmy się jak w dość prosty sposób możemy dodać do naszych testów sprawdzanie treści pdfów. Oczywiście jeżeli sam pdf będzie bardziej zaawansowany to pobranie treści będzie trudniejsze, jednak jeżeli nasz pdf to głównie tekst z ostylowaniem, to warto spróbować zaimplementować kilka testów. Czasami można znaleźć ciekawe błędy, że np. pdf nie zawiera żadnej treści.


najwięcej ofert html

Zdjęcie główne artykułu pochodzi z stocksnap.io.

 

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://geek.justjoin.it/sprawdzanie-tresci-pobranego-pliku-pdf-selenium-webdriver-c-oraz-itextsharp/" order_type="social" width="100%" count_of_comments="8" ]