Nobody expects the Spanish Inqiuisition! …czyli testy UI

5

Wielu z was zapewne zna powyższy skecz z repertuaru Monty Pythona (jeśli nie to serdecznie polecam). Pytanie tylko, jak to się ma do jakości, testów automatycznych i innych tego typu rzeczy…

 Autor tekstu: Krzysztof Turowski. QA Manager w firmie Ivanti (Diamentowy Sponsor konkursu Daj Się Poznać 2017). Absolwent wydziału Informatyki i Zarządzania Politechniki Wrocławskiej.
Fan testów automatycznych w programowaniu zwinnym. Promuje BDD, tworzenie jasnych i przejrzystych wymagań, oraz świadome podejście całego zespołu do jakości wytwarzanego oprogramowania. Tryska energią, zawsze wesoły i uśmiechnięty.

Błędy były, są i będą. Niestety nie da się ich uniknąć. Nawet najlepiej przetestowany produkt będzie je posiadał. O ile w fazie developmentu możemy się z nimi pogodzić, o tyle te, które pojawią się u klienta, będą kosztowały nas o wiele więcej. Dodatkowo – zgodnie z prawem Murphy’ego – pojawią się dokładnie wtedy, kiedy najmniej się ich spodziewamy.

Na szczęście wielu z nich możemy uniknąć.

Rozwiązań jest kilka, aczkolwiek tylko świadome podejście i dołączenie testów UI-owych da nam pełny pogląd na aplikację, jakość, oraz zwiększą prawdopodobieństwo, że produkt będzie działać tak, jak tego oczekujemy. Stosując odpowiednią strategię, uda się nam otrzymać stosunkowo relatywnie niskie koszty zarówno tworzenia, utrzymania i rozwijania testów.

Tools

Chciałbym szczególnie zwrócić uwagę na framework Selenium, służący do automatyzacji testów m.in. aplikacji webowych.

Webdriver Selenium poradzi sobie z wieloma przeglądarkami – od Chrome i Firefoxa, przez wiele wersji IE na Safari i Operze kończąc. Do tego jego API współpracuje z wieloma językami jak: C# , Java, JavaScript, Ryby, Python i wiele innych.

Aby zacząć swój projekt potrzebujecie następujących nugetów:

Chromedriver-ów jest wiele. Dla tego przykładu użyłem drivera autorstwa jsakamoto (https://github.com/jsakamoto/nupkg-selenium-webdriver-chromedriver/ ). Zasłużył sobie na uwagę między innymi niewielkimi rozmiarami (jedynie 680K).

Pierwsze koty

Oto najprostszy przykład wzorowany na dokumentacji Selenium.

Kilka poniższych linijek porusza dwie niesamowicie istotne kwestie, o których większość piszących kod nie dba na samym początku – selektory i waity.

public class FirstTest
{
    [Test]
    public void DajSiePoznac()
    {
        using (IWebDriver driver = new ChromeDriver())
        {
            driver.Manage().Window.Maximize();
            driver.Navigate().GoToUrl("http://devstyle.pl");
 
            IWebElement searchFieldElement = driver
                .FindElement(By.Name("s"));
            IWebElement searchButtonElement = driver
                .FindElement(By.ClassName("search-button"));
 
            searchFieldElement.SendKeys("Daj się poznać");
            searchButtonElement.Click();
 
            var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
            wait.Until(
                d => d.Title.StartsWith(
                    "You searched", StringComparison.OrdinalIgnoreCase
                )
            );
 
            Assert.AreEqual(
                "You searched for Daj się poznać | devstyle.pl | Maciej Aniserowicz", driver.Title
            );
        }
    }
}

Co się stało?

Jeśli go uruchomicie – pamiętając o odpowiednich referencjach – to zauważycie, że nasz program otworzy okno przeglądarki, a następnie poda adres http://devstyle.pl . Do elementu o nazwie ‘s’ zostanie wysłany tekst „Daj się poznać”. Po wpisaniu tekstu klikniemy w ikonę lupy.

Następnie nasza aplikacji poczeka nie więcej niż 10 sekund na to, aby nazwa nowej strony zaczęła się od „You searched”. Dodatkowo sprawdzimy, czy tytuł strony jest tym, czego oczekujemy.

Prawda, że proste?

Istotne jest to, że nasz webdriver “porusza się” po stronie internetowej za pomocą selektorów. Dzięki nim lokalizujemy elementy i możemy wymusić na nich różne działania. Mamy kilka możliwości lokalizacji:

  • ID,
  • nazwa klasy lub CSS Selektor,
  • nazwa atrybutu,
  • tekst z linku,
  • XPath

Powyższa kolejność ma istotne znaczenie w przypadku pisania kodu.

Naszym pierwszym wyborem powinien być ID. Jest on unikatowy i najczęściej najbardziej czytelny.

Kolejnym elementem jest nazwa klasy, polecana również ze względu na swoją ogromną łatwość odszyfrowania co się pod nią kryje.

Na równi z nazwą klasy stawiam CSS selektor. Przy czym musimy dbać o to, żeby nasze selektory były czytelne!

Możemy także użyć wartości atrybutu, lub nawet części tekstu z adresu linku.

XPathowe wtrącenie

Na samym końcu plasuje się XPath, którego popularności nie jestem w stanie zrozumieć.

Wiele osób mających pojęcie odnośnie automatyzacji używa na co dzień XPathów i nawet uważa, że rozumie (sic!) co się pod nimi kryje. Dla mnie w większości jest to brak dobrego nauczyciela, brak czasu lub lenistwo.

Nie wierzę, aby ktokolwiek stosujący XPath był dumny np. z takiego zapisu (swoją drogą zawiera on w sobie ID). Przy okazji: jest to przycisk “Szczęśliwy traf” Google. ;)

*[@id="tsf"]/div[2]/div[3]/center/input[2]

Oczywiście są przykłady, kiedy Xpath znajduje zastosowanie i czasem po prostu trzeba się nim posłużyć. Aczkolwiek jeśli ktoś robi to nagminnie, to bardzo szybko się pogubi i zniechęci zarówno siebie jak i innych kiedykolwiek zaglądających w jego kod.

…but WAIT!

Drugą bardzo istotną kwestią są metody Wait.

Niestety, nasze aplikacje nie zawsze działają tak, jak tego chcemy. Może okazać się, że zamiast zielonego testu zobaczymy NoSuchElementException pomimo, że na naszej stronie element jest.

Komputer bywa szybszy niż nasze oko i w trakcie wykonania testu żądany element może nie zdążyć się załadować! Jest to jeden z najczęstszych problemów, z którymi borykają się wszyscy stawiający swoje pierwsze kroki z automatyzacją.

Oczywiście bardzo mocno kusi Thread.Sleep() , ale jest to bardzo zła praktyka. Polecam natomiast wszelkie wait.Until(ExpectedCondition). Warto sprawdzić możliwości tych metod. Oszczędzą one duuuużo pracy i problemów w przyszłości.

Więcej podstawowych podstaw

Poniżej przejrzymy kilka podstawowych metod webrivera:

Te, dzięki którym możemy obsługiwać przeglądarkę:

driver.Navigate().GoToUrl("http://www.google.com/");
driver.Navigate().Back();
driver.Navigate().Forward();
driver.Navigate().Refresh();
driver.Manage().Window.Maximize();

driver.SwitchTo().Alert();

Możemy wyszukiwać element i… użyć go: kliknąć, pobrać tekst, porównać wartość czy też wyczyścić..

IWebElement element = driver.FindElement(By.Id("element"));
element.click();
element.sendKeys();
element.clear();
element.getText();
 
// ... etc.

Istotnym elementem driver są także akcje. Jest to zbiór bardziej złożonych czynności, wykraczających poza operacje na pojedynczym elemencie:

Actions coolAction = new Actions(driver);
 
coolAction
    .ClickAndHold(driver.FindElement(By.Id("someElement")))
    .MoveToElement(driver.FindElement(By.Id("coolElement")))
    .SendKeys("someText")
    .Release()
    .Perform();

Fajne, prawda?

Page Object Model: zmienność jest w stylu styli!

Przy testach UI prawie zawsze pojawia się zarzut, że każda zmiana klas czy idków spowoduje błędy we wszystkich testach korzystających z tego opisu.

Jest pewne rozwiązanie, które nazywamy Page Object Modelem. Pomaga ono umiejscowić wszystkie elementy strony w jednym miejscu w kodzie naszych testów. Dzięki temu nie musimy szukać wywalających się elementów, rozproszonych po całym projekcie testowym.

Uwierzcie, że przy 500+ testach i braku takiej organizacji zmiana selektorów może okazać się ogromnym problemem. Właściwie pracą na pełen etat.

Niestety, nie ma innego sposobu niż uruchamianie testów zawsze, po każdej zmianie ID lub klasy. A następnie poprawienie Page Object Modeli. Zysk z testów UI-owych spokojnie pokryje nam koszt pracy poniesiony przy robieniu tego na bieżąco.

Poniżej znajdziecie przykład tego, jak wygląda test korzystający z POM. Zastosowanie tego wzorca pozwala nam na zdecydowaną poprawę czytelności kodu. Staje się on o wiele łatwiejszy w utrzymaniu. Szczególnie, kiedy dokonujemy zmian w strukturze strony! Dodatkowo zyskujemy możliwość re-używania metod.

[Test]
public void DajSiePoznac()
{
    var dajSiePoznacPage = driver.NavigateTo<DajSiePoznacPage>();

    dajSiePoznacPage.SearchText = "Daj się poznać";
    var searchResultsPage = dajSiePoznacPage.SearchForText();

    Assert.True(
        searchResultsPage.MainHeading.Contains("SEARCH RESULTS: DAJ SIĘ POZNAĆ")
    );
}

Jest to troszkę zmodyfikowany przykład z wcześniej. Użyłem innej asercji: teraz sprawdzam tekst z poniższej ramki.

clip_image001

A gdzie tutaj Page Object Model? Proszę bardzo!

Warto zaopatrzyć się w bazową klasę dla wszystkich tworzonych w ten sposób modeli:

public class BasePage
{
    protected IWebDriver Driver { get; }

    public string PageUrl { get; protected set; }

    public BasePage(IWebDriver driver)
    {
        Driver = driver;
        PageFactory.InitElements(Driver, this);
    }
}

Opisując stronę w kodzie skupmy się tylko na jej istotnych elementach:

public class DajSiePoznacPage : BasePage
{
    [FindsBy(How = How.Name, Using = "s")]
    private IWebElement searchText;

    [FindsBy(How = How.ClassName, Using = "search-button")]
    private IWebElement searchButton;

    public DajSiePoznacPage(IWebDriver driver) : base(driver)
    {
        PageUrl = @"http://devstyle.pl";
    }

    public string SearchText
    {
        set { searchText.SendKeys(value); }
    }

    public SearchResultsPage SearchForText()
    {
        searchButton.Click();

        return new SearchResultsPage(Driver);
    }
}

A strona z wynikami wyszukiwania wygląda tak:

public class SearchResultsPage: BasePage
{
    [FindsBy(How = How.CssSelector, Using = ".main-heading")]
    private IWebElement mainHeading;

    public string MainHeading => mainHeading.Text;

    public SearchResultsPage(IWebDriver driver) : base(driver)
    {
    }
}

Jak myślicie, dlaczego w tym przypadku używam metody Contains oraz niepełnego tekstu: “Search Results: Daj się poznać”?

Dodatkowo celowo usunąłem metodę Wait. Bez niej scenariusz jak najbardziej zadziała, ale warto pomyśleć nad tym, gdzie można ją zastosować.

A sam scenariusz… chwilę trwa. Jak myślicie: co jest jego słabym punktem i jak to poprawić?

Ile zalet!

Dzięki automatyzacji testów zawsze będziecie wiedzieć, jaki jest stan Waszej aplikacji.

To WY decydujecie czy i kiedy chcecie przetestować Wasz system. Jakie funkcjonalności, scenariusze chcecie sprawdzić. Testy regresyjne nie będą spędzały Wam już snu z powiek. Mało tego – będą działały wtedy, kiedy śpicie, do tego w godzinę lub kilka godzin, a nie w kilka dni. Jedyne, co należy zrobić, to włączyć je w proces CI i CD (Continuous Integration i Continuous Deployment).

Do tego dosłownie w kwadrans będziecie wiedzieć, jak bardzo namieszaliście. Może nie w całym systemie, ale na pewno w podstawowych funkcjonalnościach. Nazywamy to “smoke testami“. Takie testy powinny trwać do 15 minut i sprawdzać podstawowe działanie programu. Najlepiej, gdyby były uruchamiane automatycznie po każdym wdrożeniu.

Ba, nauczycie się Waszego systemu i poznacie go jeszcze lepiej! Nie tylko z punktu widzenia developera i testów jednostkowych, ale także użytkownika końcowego. Będziecie musieli zrealizować konkretny scenariusz od A do Z. Poczynając od przygotowana systemu aż po zakładany wynik. Tworząc scenariusz testowy musicie fizycznie odtworzyć te kroki, a nic nie daje lepszej wiedzy na temat aplikacji niż jej używanie.

Dodatkowo odkryjecie masę błędów na etapie tworzenia. Przede wszystkim dlatego, że będziecie używać Waszego własnego produktu. Testy UI zakładają, że pokrywamy nimi wszystkie podstawowe scenariusze… a także te najbardziej odjechane (corner case). Zatem skoro zamierzacie utworzyć określony scenariusz, to logiczne jest, że musi on działać. A jeśli nie działa, to znaczy, że najprawdopodobniej gdzieś ukrył się mały (lub duży) bug.

Wyrobicie w sobie dobre praktyki brania większej odpowiedzialności za Wasz produkt. Od tej pory nie będzie to tylko zbiór klas, metod, obiektów i funkcji. Wasz kod stanie się dodatkową dokumentacją, bazą wiedzy. Miejscem, w którym tworzycie i sprawdzacie to, co udało się napisać. Pola na formularzu będą nosiły ludzkie IDki, a CSSy będą faktycznie coś znaczyły, przestaną być tylko zbitkiem liter. Wasz kod zacznie być “widać” także poza konsolą i źródłami.

Niech pracuje maszyna!

To, co męczące dla nas, nie zmęczy komputera! Jego nie zaboli to, że po raz n-ty będzie musiał wykonywać ten sam test na wszystkich przeglądarkach. A uruchamianie produkt na wielu platformach i przeglądarkach w tym samym czasie powinno być regułą.

Mało tego, maszyna nie pomyli się w tym! Nie zapomni o żadnym kroku, nie zgłosi buga przez nieuwagę.

Zrobi to także niezależnie i każdorazowo wyczyści cache.

Będziecie mogli wrócić do Monty Pythona ;). Bo bugi więcej Was nie zaskoczą.

Testy automatyczne nie służą do tego, aby zastąpić ludzkie oko. I pewnie długo jeszcze nie zastąpią. Ale mogą dać poczucie bezpieczeństwa.

Link do projektu : https://github.com/kritur/DajSiePoznac

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

5 Comments

  1. Radek Maziarka on

    Dokładnie o tym rozmawialiśmy na ostatnim WROC# Online – testy UI pozwalają uchronić nas przed takimi rodzajami problemów, przy których testy unitowe nie dadzą rady. Utrzymywanie testów i zmiany klas / identyfikatorów w testach po zmianie ich na UI wymaga uwagi i praktyki, ale na dłuższą metę pozwala na daje duże korzyści w postaci pewności że nasza aplikacja pisana kilkanaście miesięcy wstecz działa tak jak powinna. W testy UI warto zainwestować gdy mamy aplikację głównie front-endową – dużo logiki po stronie użytkownika, skomplikowany panel zarządzania, kolejne warstwy funkcjonalności gdzie każdy następny krok zależy od kilkunastu poprzednich.

  2. Agr… Drodzy testerzy, wspominajcie w czym testujecie. Jak wspomniano w artykule Selenium współpracuje z wieloma językami więc taka informacja może być przydatna. Autor używa .NET, a przynajmniej NUnit jest dla .NET :)

    W moim przypadku (Java) do wstrzymania akcji lepiej sprawdzało się

     TimeUnit.SECONDS.sleep(2); 

    niż użycie WebDriverWait. Dlaczego? Nie wiem, ale WebDriverWait wyrzucał błędne wyniki.

    • Sleep działa fajnie raz, drugi, trzeci. Co w przypadku kiedy Twój set testów urośnie do 500+ i każdy z nich będzie miał jakiegoś sleepa?
      Szczególnie testerzy powinni zwracać uwagę na jakość swojej pracy i nie dopuszczać do skracania sobie drogi.
      To co mogę polecić, to po pierwsze sprawdzenie co dokładnie i kiedy się dzieje że wait wyrzuca błędy ( i jakiego typu błędy to są). Stackoverflow jest świetnym miejscem żeby zacząć szukać. Zawsze możesz też sam/sama zagaić ;)
      Jeśli piszesz w Javie, to pewnie Twój dev team również pisze w Javie. Zapytaj, pogadajcie, pokaż problem – popracujcie razem nad jakością zarówno Twojego kodu, a co za tym idzie całej aplikacji.
      Wierzę że dasz radę :)

      • Niestety, dopiero poszukuję pracy jako tester – nie koniecznie jako automatyczny. Zdaję sobie sprawę, że sleep nie jest najlepszym rozwiązaniem. Sam WebDriverWait nie wywala błędów, po prostu raz wynikiem testu jest 20, innym 26 a prawidłowym wynikiem powinno być 42. Tak na pierwszy rzut oka – nie czeka aż dostępny będzie element i to jest problemem.
        Do przetestowania było sprawdzenie, przy jakiej wartości pola Age ładuje się konkretna strona. Żeby to zweryfikować porównywałem tekst z , bo tylko taki był dostępny.
        Na pewno jeszcze popróbuję z WebDriverWait, bo jak piszesz – warto.

      • Powinno być:
        Żeby to zweryfikować porównywałem tekst z

         <body> 

        bo tylko taki był dostępny.