3 sprawdzone sposoby na złe testy automatyczne

3 sprawdzone sposoby na złe testy automatyczne
Jak wykonywać i jak nie wykonywać automatyzacji, okiem nowego autora na naszych łamach, Michała Jawulskiego.
 

Od ponad roku rozwijam framework do automatyzacji testów. Przez ten czas miałem okazję pracować przy kilku projektach związanych z automatyzacją, gdzie poznałem wspaniałych ludzi i za każdym razem miałem okazję spojrzeć na testowanie automatyczne z innej perspektywy. Widziałem kod doskonały (tak, istnieją takie! :)) oraz taki, o którym chciałbym jak najszybciej zapomnieć. Dzisiaj skupię się na tym drugim i przedstawię Wam trzy sprawdzone sposoby na to, jak zepsuć testy automatyczne.

Na początku wyjaśnijmy krótko, czym są testy automatyczne. Są to testy, które należą do tej samej grupy co testy manualne (czyli sprawdzamy działanie programu korzystając z niego w taki sam sposób jak użytkownik końcowy, działając według określonego wcześniej planu - scenariusza testowego). Od testów manualnych odróżnia je to, że są wykonywane… automatycznie! (zaskakujące, prawda?) Zamiast klikać po okienkach ręcznie, piszemy program, który wykona to za nas, a wynikiem pracy takiego automatu będzie raport z informacją o sukcesie bądź niepowodzeniu testów. Wykorzystanie komputera do tego celu daje nam niezawodność (komputer zawsze będzie działał w ten sam sposób), a także oszczędność pieniędzy i czasu (maszyna może działać 24 godziny na dobę i kosztuje mniej pieniędzy niż praca człowieka).

Testowanie automatyczne, czy też automatyzacja testów, staje się w ostatnich latach coraz popularniejszym trendem. Na przestrzeni ostatnich lat zauważalny jest niemal stale rosnący wzrost zainteresowania tą tematyką (Rys. 1). Powstaje coraz więcej frameworków pomagających automatyzować niemalże wszystkie aplikacje napisane w różnych technologiach. W związku z tym jest też coraz więcej kodu określanego nie jako kod aplikacji, ale jako kod testujący.

 

Rys 1. Wykres Google Trends dla zapytania “automation testing”: https://trends.google.pl/trends/explore?date=2007-06-21%202017-06-21&q=automation%20testing

 

Trend ten jest na tyle nowy, że nie zdążyły się jeszcze wykształcić dobre praktyki, zasady czy wzorce pisania kodu testów automatycznych. Na szczęście powoli zaczyna się to zmieniać.

Aby lepiej zilustrować to zjawisko warto posłużyć się przykładem. W tym przypadku będzie to aplikacja oraz projekt testów automatycznych, które powstały specjalnie na potrzeby tego artykułu. Wszystkie źródła dostępne są na https://github.com/mjawulski/hownottoautomate.

 

 

Rys 2. Ekran logowania aplikacji, którą będziemy automatyzować.

 

Poniżej przypadek testowy, który będziemy automatyzować:

Krok

Akcja

Oczekiwany rezultat

1

Uruchom aplikację

Okno logowania zostaje wyświetlone

2

Wpisz login @login i hasło @password

-

3

Kliknij przycisk @button

Okienko logowania znika. Pojawia się główne okno aplikacji. Przeciętny użytkownik widzi zwykłe okno, a administrator widok rozszerzony.

 

oraz dane testowe wykorzystywane podczas testów:

Iteracja

@login

@password

@button

1

user1

secret

Login

2

admin

supersecret

Admin login

 

 

Implementacja takiego przypadku testowego może wyglądać następująco:

 

[TestMethod]

publicvoid UserCanLoginToApplication()

{

  IAppapp = new App();

  ILoginWindowloginPage = new LoginWindow();

  IWindowadminMainWindow = new AdminMainWindow();

  IWindowuserMainWindow = new MainWindow();

 

  stringlogin = GetTestParameter("login");

  stringpassword = GetTestParameter("password");

          

  app.Launch();

  Assert.IsTrue(app.IsLaunched);

 

  loginPage.LoginTextBox.Text = login;

  loginPage.PasswordTextBox.Text = password;

  boolisAdmin = login == “admin”;;

 

  if(isAdmin)

  {

    loginPage.AdminLoginButton.Click();

    DateTimestart = DateTime.Now;

    while(!adminMainWindow.IsLoaded && (DateTime.Now-start).TotalSeconds < 5)
    {

      Thread.Sleep(100);

    }

    Assert.IsTrue(adminMainWindow.IsLoaded);

  }

  else[1]

  {

    loginPage.LoginButton.Click();

    DateTime start = DateTime.Now;

    while(!userMainWindow.IsLoaded && (DateTime.Now-start).TotalSeconds < 5)
    {

      Thread.Sleep(100);

    }

 

    Assert.IsTrue(userMainWindow.IsLoaded);

  }

}

 

Warto przyjrzeć się omawianemu przypadkowi testowemu nieco bliżej i wyciągnąć z niego kilka wniosków. Na tej podstawie opracowałem trzy sposoby, jak NIEPOPRAWNIE robić testy automatyczne. :)

Sposób 1. Im więcej kodu w klasie testowej, tym lepiej.

Pierwszą rzeczą, którą chciałbym poruszyć jest nadmiar kodu deweloperskiego w klasie testowej.

W przypadku aplikacji desktopowej, webowej czy mobilnej, produktem sprzedawanym klientowi jest gotowy program. W przypadku projektu testów automatycznych jest nim właśnie kod testów automatycznych. Dlatego jeśli chcesz to zrobić źle, to umieść w klasie z testami instrukcje warunkowe, pętle itp., które w przeciwnym razie mógłbyś po prostu schować głębiej w kodzie. 

Testy powinny charakteryzować się stabilnością oraz łatwością w utrzymaniu, nawet przez osoby nietechniczne. Poza tym, o wiele łatwiej zrozumieć, o co chodzi w teście, jeśli nie musimy przebijać się przez niepotrzebną logikę.

Jak sobie jednak radzić, gdy dochodzi do takiej sytuacji? Z pomocą przychodzi wzorzec projektowy Page Object Model (POM). Dobrze opisał go Martin Fowler na swoim blogu.

Każde okienko, strona, a nawet grupa elementów funkcjonalnie powiązanych ze sobą na jednym oknie, stanowi POM. W każdym POM-ie implementowana jest funkcjonalność elementów znajdujących się w nim. Można więc zaimplementować klikanie w poszczególne elementy, wprowadzanie tekstu, a nawet bardziej złożone akcje, obejmujące kilka pojedynczych działań, jak logowanie.

Spójrzmy na nasz przykład. Można w nim znaleźć kilka POM-ów: okienko logowania oraz strony, do których kierowany jest użytkownik po zalogowaniu. Innymi przykładami często spotykanych POM-ów mogą być menu na stronie, stopka, nagłówek strony www czy wreszcie różnego rodzaju okienka modalne. Stosując ten wzorzec projektowy, możemy usunąć logikę znajdującą się w testach i przenieść ją do innego, bardziej odpowiedniego miejsca.

Przykładowy POM prezentuję poniżej:

 

publicclass LoginWindow : ILoginWindow

{

        //Do ustawienia kontrolek można użyć frameworka (np TestStack.White)

        public TextBox LoginTextBox { get;}

        public TextBox PasswordTextBox { get;}

        public Button LoginButton { get;}

        public Button AdminLoginButton { get;}

 

        public void Login(string login, string password)

        {

            LoginTextBox.Text = login;

            PasswordTextBox.Text = password;

            if (login.ToLower()==”admin”)

            {

                        AdminLoginButton.Click();

            }

            else

            {

                        LoginButton.Click();

            }

       }

}

 

Sposób 2. Udostępniaj kontrolki. Wszędzie. Bez zastanowienia.

Po usunięciu niepotrzebnych fragmentów kodu z klasy testowej i przeniesieniu ich do POM-ów, można pokusić się o udostępnienie kontrolek z POM-u do testów. Często spotykanym poleceniem w przypadkach testowych jest “Kliknij w przycisk...”.

Interfejs POM-u z powyższego przykładu może wyglądać tak:

 

publicinterface ILoginWindow

    {

        TextBox LoginTextBox { get; set; }

        TextBox PasswordTextBox { get; set; }

        Button LoginButton { get; set; }

        Button AdminLoginButton { get; set; }

 

        bool Login(string login, string password);

    }

 

Dzięki temu w teście możemy napisać taką linijkę:

 

publicvoid UserCanLoginToApplication()

{

  …

  //Step 3

  loginWindow.LoginButton.Click();

  …

}

 

To dobre rozwiązanie, o ile jego autor jest świadomy konsekwencji stosowania go. W przypadku obiektów typu Button, CheckBox, Dropdown można uzasadnić udostępnienie metody Click(), a w przypadku obiektów typu TextBox metod typu SetText(), GetText(). Dyskusyjne jest natomiast udostępnianie pozostałych metod i właściwości tych obiektów.

Należy pamiętać, że w wielu frameworkach kontrolki posiadają metody zwracające obiekty, do których przeciętny użytkownik piszący testy automatyczne, z reguły nie powinien mieć dostępu (np. obiekt AutomationElement z frameworka UIAutomation dla projektów aplikacji desktopowych pisanych w technologiach .NET). Ukrywanie obiektów pozwala zachować większy poziom “bezpieczeństwa kodu”, co sprawi, że użytkownik implementujący testy nie wywoła wtedy nieświadomie niepotrzebnej metody. Pamiętajmy, że proponowane rozwiązanie powinno być dostosowane do potrzeb i umiejętności użytkownika końcowego.

Jeśli autor testu rozważa, czy warto udostępnić kontrolki na zewnątrz POM-u, powinien zastanowić się, jak często będzie z danej kontrolki korzystać. Jeśli zostanie podjęta decyzja o zastąpieniu standardowych kontrolek takimi, które zostały wyprodukowane przez firmy trzecie, np. Telerik, wszystkie dotychczasowe kontrolki muszą zostać zastąpione nowymi. Na ogół konieczne będzie też przemapowanie niektórych właściwości. Zazwyczaj  nie jest to bardzo złożony proces — przy dzisiejszych możliwościach środowisk programistycznych refactoring tego typu może sprowadzić się do trzech kliknięć i dwóch uderzeń w klawisz Enter. Ale nie musi. Rozwiązaniem problemu jest wtedy nieudostępnianie kontrolek na zewnątrz i rozważanie stworzenia metod pozwalających na udostępnianie ich właściwości. Dzięki temu zyskać można większą kontrolę nad możliwościami, jakie autor testu daje potencjalnemu użytkownikowi stworzonego przez siebie POM. Interfejs po zmianach mógłby wyglądać następująco:

 

    publicinterface ILoginWindow

    {

        void ClickButton(string buttonName);

        voidSetText(string text, string textBoxName);

        stringGetText(string textBoxName);

 

        bool Login(string login, string password);

    }

 

Sposób 3. Asercje, które nic nie mówią.

Kolejnym krokiem po napisaniu wszystkich POM i zaimplementowaniu przypadków testowych jest uruchomienie testów. Jeśli testy świecą się na zielono, czyli zakończyły się powodzeniem, wszystko jest w porządku.

 

 

Jeśli jednak autor kodu otrzyma raport z pozycją oznaczoną na czerwono (Rys. 3), oznacza to, że test nie zakończył się powodzeniem i należy przeanalizować błędy. Od czego zacząć?

Większość testerów zacznie od zajrzenia do logów. I po poświęceniu na ich przejrzenie mnóstwa czasu znajdzie w nich tylko jedną informację, która wyglądać będzie mniej więcej tak:

 

 

Taka informacja mówi nam, że asercja nie została spełniona. Tyle tylko, że jest to oczywiste — test się przecież nie udał. Po uzyskaniu tej zupełnie nieprzydatnej informacji, zazwyczaj tester zaczyna analizować Stack Trace. Dowie się, że błąd pojawił się w linijce 38. Pozostanie jeszcze tylko zajrzeć do kodu, żeby dowiedzieć się, że linijka 38 wygląda mniej więcej w ten sposób:

Assert.IsTrue(adminMainWindow.IsLoaded);       

 

Dopiero w tym momencie dowiadujemy się, jaka jest przyczyna błędu - okienko z logowania nie zostało wyświetlone. W tym przypadku znalezienie przyczyny błędu zajęło nam dosłownie sekundę, ale w przypadku bardziej skomplikowanych projektów trwałoby to o wiele dłużej. Na szczęście możemy ten czas oszczędzić, zmieniając powyższą linijkę na:

Assert.IsTrue(adminMainWindow.IsLoaded,"Admin Window was not loaded");

 

Wszystkie frameworki do testów, z których miałem okazję korzystać (MSTest, NUnit, JUnit) posiadają różne wersje metody Assert, w których możemy podać komunikat wyświetlany wtedy, gdy asercja nie zostanie spełniona. Jeśli framework, który jest używany w projekcie, nie posiada takiej możliwości, należy go zmienić na alternatywny. Dzięki temu w przypadku błędu w logach znajdzie się odpowiednia informacja. Na przykład taka:

 

 

Przedstawiony powyżej test po wprowadzeniu wszystkich opisanych zmian wygląda następująco:

 

        [TestMethod]

        public void ICanLoginWithCorrectCredentials()

        {

            IApp app = new App();

            ILoginWindow loginPage = new LoginWindow();

 

            string login = GetTestParameter("login");

            string password = GetTestParameter("password");

 

            //Step 1

            app.Launch();
            Assert.IsTrue(app.IsLaunched, "App was not launched”);

 

            //Step 2

            Assert.IsTrue(loginPage.Login(login, password), "Cannot login with given credentials");

        }

 

Opisane podejście ma charakter Step by Step. To alternatywa dla dobrze znanego Arrange-Act-Assert (AAA). W przypadku testów automatycznych, w których implementowany jest przypadek napisany krok po kroku, takie podejście sprawia, że kod staje się bardziej czytelny - szczególnie dla osób nietechnicznych, które będą musiały w późniejszym czasie ten kod utrzymywać bądź rozwijać. Przeniesienie jednego kroku scenariusza testowego na linijkę kodu pozwoli im łatwiej i szybciej zrozumieć logikę testu.

Dobre praktyki i wzorce w zakresie testowania automatycznego dopiero zaczynają powstawać. Warto stosować je w codziennej pracy, dzięki czemu będzie ona przyjemniejsza!  Jeśli chcecie rozwijać swoją wiedzę, polecam wam kilka sposobów:

  1. Czytajcie mojego bloga. Co jakiś czas będę zamieszczał teksty związane z tym tematem.
  2. Poszukajcie, czy w waszym towarzystwie nie znajduje się ktoś od kogo moglibyście się uczyć. Z własnego doświadczenia wiem, że chcąc rozwinąć się w dowolnym aspekcie nie do przecenienia jest pomoc takiej osoby.
  3. Poszukajcie, czy w waszej okolicy nie są organizowane meetupy związanie z testowaniem automatycznym.

 

O autorze

Michał Jawulski - autor bloga www.mjawulski.pl, na którym pisze o programowaniu. Lubi poznawać nowe technologie, pasjonuje go inżynieria oprogramowania. Pracuje we wrocławskim oddziale firmy Capgemini.