Chyba nikogo nie trzeba przekonywać w dzisiejszych czasach do testów bardzo złożonych aplikacji webowych. Choć znajdą się przypadki, gdzie tych testów nie ma w ogóle, bo… (tu wstaw cokolwiek). Pisanie testów do działającego już kodu wydaje się być zbędną czynnością, której programiści nie chcą wykonywać. Daje się je juniorom, aby „poznali” lepiej projekt. Testy traktuje się po macoszemu, a sama ich obecność jest nieco uciążliwa w sytuacji, gdy założenia projektowe zmieniają się jak w kalejdoskopie. Ale można sobie ułatwić życie korzystając z Selenium.

Testy z założenia powinny być krótkie. Testować jedną rzecz na raz, wykonywać się szybko, być powtarzalne i łatwe do modyfikacji. Do tego należy dodać kulturę testowania w organizacji, mówiąca o tym jak piszemy testy, co testujemy, a czego nie, mając na uwadze ograniczenia projektowe (ludzie, czas, budżet).

Dobrym punktem startu, jeśli mamy już kod, jest użycie testów automatycznych end-to-end z wykorzystaniem Selenium.

Za tą opinią stoi kilka faktów:

  • uruchomienie serwera Selenium jest banalne,
  • składnia testów jest prosta i intuicyjna, nawet dla juniora,
  • ogromna społeczność używająca Selenium służy swoją pomocą.

Testy takie dadzą nam podstawy do dalszego stabilizowania naszej pracy z kodem. Są takim kanarkiem w kopalni kodu mówiącym o tym, że coś poszło nie tak. Dodatkowo działają „najbliżej” prawdziwego użytkownika. W zasadzie powtarzają jego zachowania, które dodatkowo można nawet obserwować w oknie przeglądarki używanej do testów. Czasami jest to hipnotyzujące doświadczenie widzieć jak w wyżej wymienionym oknie automat uzupełnia na przykład pola formularza rejestracyjnego.

Zacznijmy testy

Pierwszym krokiem w użyciu testów automatycznych jest uruchomienie serwera Selenium. Testy mogłyby się odbyć bezpośrednio w kodzie, wtedy przeglądarka uruchamiana jest bezpośrednio na komputerze osoby testującej lub serwera testowego. Można od razu skorzystać z obrazu Docker z serwerem selenium standalone. Repozytorium z obrazami znajduje się w repo https://hub.docker.com/u/selenium.

Testując lokalnie możemy natrafić na problemy związane z wersją przeglądarki, ścieżką do niej, a wyskakujące okno z czasem może stać się irytujące. Pozostaje opcja headless, która ukrywa okno, lecz mam z tą opcją złe skojarzenia, gdyż nie zawsze testowana aplikacja działała jak należy. I to pomimo stosowania takich samych opcji uruchomienia jak bez headless. Rozsądnym wyjściem podczas pisania testów jest użycie ich lokalnie. A następnie, po ich napisaniu, podmiana klasy testującej z lokalnej na tę uruchamianą na serwerze Selenium.

Jak to wygląda u mnie?

W swoich projektach, które nie tylko slużą do testów automatycznych, używam Selenium wykorzystując do tego celu dwa wzorce projektowe: adapter oraz łańcuch zależności.

Pierwszego z nich używam jako swoistej nakładki na metody drivera selenium, co upraszcza mi niejednokrotnie korzystanie. Przykładowo klasa DriverProxy będąca adapterem na klasę Driver wygląda następująco.

public class DriverProxy
    {
        private IWebDriver _driver;
        private IJavaScriptExecutor _js;

        public void InitDriver()
        {
             _driver = new ChromeDriver(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
            ImplicitWait(5);
            _js = (IJavaScriptExecutor)_driver;
        }

        public void QuitDriver()
        {
            _driver?.Quit();
        }

        public void Wait(string milisec)
        {
            _js.ExecuteAsyncScript("window.setTimeout(arguments[arguments.length - 1], " + milisec + ");");
        }

        public void Scroll(int x, int y)
        {
            _js.ExecuteScript("window.scrollBy(" + x + "," + y + ")", "");
        }

 public IWebElement FindElement(By selector)
        {
            return _driver.FindElement(selector);
        }

        public void SwitchToFrame(string iframe)
        {
            _driver.SwitchTo().Frame(iframe);
        }

        public void ImplicitWait(double seconds)
        {
            _driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(seconds);
        }

        public IEnumerable<IWebElement> FindElements(By cssSelector)
        {
            return _driver.FindElements(cssSelector);
        }

        public void NavigateToUrl(string url)
        {

            _driver.Navigate().GoToUrl(url);
        }


}

Jak można zaobserwować na powyższym listingu, klasa DriverProxy upraszcza zapis i wywołanie metod klasy Driver, co jest potwierdzeniem użycia wzorca Adapter. W szczególności na uwagę zasługuje metoda służąca do wstrzymania działania skryptu do określonego czasu np. 2 sekundy. Pozwala to dać czas stronie na załadowanie się DOM i dzięki temu uniknięciu błędu wywołanego nieznalezieniem elementu na stronie. Przydatne szczególnie podczas testów na produkcji. 🙂

Drugim stosowanym przeze mnie wzorcem jest wzorzec Łańcucha zależności. Dzięki niemu można dodawać kolejne kroki, które należy wykonać na stronie. W przejrzysty sposób pozwala to rozwiązanie na uporządkowanie czynności wykonywanych na stronie. A w razie pojawienia się błędu ułatwia lokalizację przyczyny.

Centralną klasą wzorca jest klasa DriverStep pokazana poniżej.

public class DriverStep
    {
        protected DriverStep nextStep;  

        public void AddStep(DriverStep step)
        {
            if (nextStep != null) nextStep.AddStep(step);
            else nextStep = step;
        }
        public virtual void Handle() => nextStep?.Handle();
    }


Przykładowe wykorzystanie klasy DriverStep jako kliknięcie w button mogłoby wyglądać jak poniżej.

class LinkStep : DriverStep
    {
        private readonly IWDriver _driverProxy;
        public LinkStep(IWDriver driverProxy)
        {
            _driverProxy = driverProxy;
        }
        public override void Handle()
        {
            PrepareLink();
            base.Handle();
        }

        private void PrepareLink()
        {
            string xpath = "/html/body/div[2]/a[2]";
            IWebElement button = _driverProxy.FindElement(By.XPath(xpath));

            button.Click();
        }
    }

Klasa LinkStep przyjmuje w konstruktorze adapter do drivera. Następnie przysłania metodę Handle, aby wykonać swoje zadanie kryjące się w metodzie PrepareLink. Ten krok może być poprzedzony krokiem logowania do aplikacji, którą testujemy. A po wykonaniu swojego zadania (kliknięcia w button) następny krok może sprawdzać, czy po nim wykonała się pożądana akcja. Na przykład wyświetliła się lista z danymi.

Połączenie tych dwóch wzorców działa wyśmienicie. Natomiast samo rozwiązanie można zastosować nie tylko do testów automatycznych, ale do automatyzacji uciążliwych zajęć. Na przykład:

  • pobieranie faktur z dowolnej strony,
  • wrzucanie podcastów do wybranych platform,
  • automatyzację w składania zamówień do hurtowni,
  • i wiele więcej. 🙂

By Piort