Wszystkie wpisy

Selenium Grid w Dockerze - Grid 3 i beta Grida 4
Selenium Grid w Dockerze - Grid 3 i beta Grida 4

English

Selenium Grid w Dockerze - Grid 3 i beta Grida 4

Jak w marcu 2021 stawiam Selenium Grid 3 w Dockerze (3.141.59-20210311) i co już da się zobaczyć w obrazach Grid 4 beta-1.

QA

Po co Grid lokalnie

Mój zestaw testów UI urósł do stu kilkudziesięciu przypadków i przestał się mieścić w przerwie na kawę. Puszczam je szeregowo, jeden po drugim, w jednym Chrome zainstalowanym na moim laptopie. Trwa to ponad czterdzieści minut, a przez ten czas przeglądarka skacze mi po ekranie i nie mogę na tej maszynie normalnie pracować.

Pierwszy odruch to zrównoleglić przebieg w runnerze. Ustawiam Parallelizable na poziomie klasy i dostaję cztery instancje Chrome naraz - na tym samym pulpicie, z jednym profilem użytkownika, z jednym chromedriver w PATH. Efekt jest przewidywalny: testy zaczynają migać, bo okna kradną sobie fokus, a jedna sesja potrafi wywrócić drugą.

Grid rozwiązuje dokładnie ten problem. Nie jest to narzędzie do “wielkiej farmy przeglądarek”, tylko do tego, żeby test nie sterował przeglądarką leżącą obok niego na tym samym pulpicie. Test wysyła polecenia po HTTP do huba, hub wybiera wolny node, a przeglądarka żyje w kontenerze, bez okna i bez mojego fokusu.

O tym, jak stawiam pojedynczą zależność, pisałem w Docker dla QA, a miesiąc temu przeniosłem to do pliku Compose. Farmę przeglądarek zostawiłem wtedy świadomie na osobny wpis i to jest ten wpis.

Jedna rzecz, której tu nie ma: stawiania Grida na współdzielonym serwerze pod CI. To jest inny temat, z monitoringiem, restartami i limitami pamięci. Tutaj wszystko dzieje się na mojej maszynie i po przebiegu znika.

Grid 3 czy Grid 4 w marcu 2021

Krótka odpowiedź: Grid 3.

Ostatnia stabilna linia serwera to nadal 3.141.59. Numer jest z 2018 roku i to czasem budzi nieufność, ale obrazy Dockerowe są utrzymywane dalej i to one nadają im świeżość. Wydanie, które biorę, to 3.141.59-20210311 z 11 marca, czyli sprzed czterech dni. Obrazy z tego wydania niosą Chrome 89, Firefox 86 i GeckoDriver 0.29.0. Serwer stary, przeglądarki bieżące.

Selenium 4 jest w becie. Pierwsza beta, 4.0.0-beta-1, została ogłoszona 15 lutego wraz z pakietami i obrazami. Zmienia się w niej sporo: protokół jest wyłącznie W3C, a sam Grid ma nową architekturę z szyną zdarzeń. Nowy Grid opisany jest na blogu Selenium (zapowiedź bety i opis nowego Grida) i wygląda dobrze, ale słowo “beta” traktuję dosłownie.

Moja zasada na ten moment jest prosta. Codzienny przebieg regresji stoi na Gridzie 3 z przypiętym tagiem. Beta Grida 4 stoi obok, na osobnych portach, i służy do sprawdzania, ile pracy będzie kosztowała migracja. Nie mieszam tych dwóch światów w jednym pliku i w jednym projekcie testowym.

Plik docker-compose z hubem i nodem

Wersje, na których to u mnie stoi:

$ docker version --format '{{.Server.Version}}'
20.10.5

$ docker-compose version --short
1.28.5

Silnik 20.10.5 przyjechał z Docker Desktop 3.2.1 z 5 marca. Compose trzymam na 1.28.5 z 25 lutego, to jest ta sama linia, o której pisałem w lutym, więc profile i dłuższa forma depends_on działają tak samo.

Plik docker-compose.grid.yml kładę obok testów:

version: "3.8"

services:
  selenium-hub:
    image: selenium/hub:3.141.59-20210311
    container_name: selenium-hub
    ports:
      - "4444:4444"

  chrome:
    image: selenium/node-chrome:3.141.59-20210311
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      HUB_HOST: selenium-hub
      HUB_PORT: 4444
      NODE_MAX_INSTANCES: 2
      NODE_MAX_SESSION: 2
      SCREEN_WIDTH: 1920
      SCREEN_HEIGHT: 1080

Co jest tu istotne.

HUB_HOST: selenium-hub to nazwa serwisu, a nie localhost. Node siedzi we własnym kontenerze i localhost oznacza dla niego jego samego. Compose tworzy dla projektu sieć, w której serwisy widzą się po nazwach, więc node rejestruje się pod adresem http://selenium-hub:4444.

Na hoście publikuję tylko port huba. Node nie musi być widoczny z mojej maszyny, bo to hub rozmawia z nodem, a mój test rozmawia wyłącznie z hubem.

shm_size: 2gb nie jest ozdobnikiem. Domyślne 64 MB w /dev/shm to za mało dla Chrome i przeglądarka wywraca się w środku sesji. Kto woli starszy zapis, może zamiast tego podmontować pamięć hosta:

services:
  chrome:
    volumes:
      - /dev/shm:/dev/shm

NODE_MAX_INSTANCES mówi, ile instancji Chrome może stać na tym nodzie, a NODE_MAX_SESSION, ile sesji obsłuży równolegle. Trzymam obie liczby równe i nie przesadzam z ich wysokością, bo każda sesja to osobna przeglądarka i osobny kawałek RAM-u.

Świadomie nie nadaję nodowi container_name. Dzięki temu mogę go zeskalować i mieć więcej niż jednego:

$ docker-compose -f docker-compose.grid.yml up -d --scale chrome=3

Po chwili sprawdzam, co hub o sobie sądzi:

$ curl -s http://localhost:4444/wd/hub/status
{"status":0,"value":{"ready":true,"message":"Hub has capacity","build":{"revision":"e82be7d358","time":"2018-11-14T08:25:53","version":"3.141.59"},"os":{...},"java":{...}}}

Bloki os i java skróciłem. Istotne jest "ready": true oraz to, że całość ma kształt JSON Wire z polem "status" na wierzchu, bo tak odpowiada Grid 3. Kiedy żaden node nie zdążył się zarejestrować, pod tym samym adresem zobaczę "ready": false i "message": "No spare hub capacity".

Konsola z listą podpiętych nodów jest pod http://localhost:4444/grid/console i to jest pierwsze miejsce, do którego zaglądam, kiedy test dostaje błąd o braku wolnej sesji. Sprzątam standardowo:

$ docker-compose -f docker-compose.grid.yml down

Test w C# przez RemoteWebDriver

Po stronie projektu testowego zmienia się jedna rzecz: zamiast lokalnego ChromeDriver tworzę RemoteWebDriver z adresem huba. Klient trzymam w wersji zgodnej z serwerem:

<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="NUnit" Version="3.13.1" />

I sam test:

using System;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Remote;

[TestFixture]
public class KoszykTests
{
    private IWebDriver _driver;

    [SetUp]
    public void SetUp()
    {
        var options = new ChromeOptions();
        options.AddArgument("--window-size=1920,1080");

        var gridUrl = Environment.GetEnvironmentVariable("GRID_URL")
                      ?? "http://localhost:4444/wd/hub";

        _driver = new RemoteWebDriver(
            new Uri(gridUrl),
            options.ToCapabilities(),
            TimeSpan.FromMinutes(2));
    }

    [Test]
    public void DodanieProduktuZmieniaLicznikKoszyka()
    {
        _driver.Navigate().GoToUrl("http://host.docker.internal:58080/sklep");
        _driver.FindElement(By.CssSelector("[data-test=add-to-cart]")).Click();

        var licznik = _driver.FindElement(By.CssSelector("[data-test=cart-count]"));
        Assert.That(licznik.Text, Is.EqualTo("1"));
    }

    [TearDown]
    public void TearDown()
    {
        _driver?.Quit();
    }
}

Ścieżka /wd/hub jest tu obowiązkowa, bo Grid 3 mówi protokołem JSON Wire pod tym właśnie adresem. Sam adres wyciągam do zmiennej środowiskowej, żeby ten sam kod dało się później wskazać na inną instancję bez rekompilacji.

Trzeci argument, czyli TimeSpan.FromMinutes(2), to nie przesada. Domyślny limit komendy w kliencie jest krótki, a pierwsze żądanie po starcie Grida czeka na przydzielenie sesji i na wystartowanie przeglądarki w kontenerze. Bez wydłużenia tego limitu dostawałem timeouty tylko na pierwszym teście w przebiegu.

Adres aplikacji pod testem to osobna pułapka. Przeglądarka nie działa już na mojej maszynie, więc localhost w GoToUrl wskazywałby na wnętrze kontenera z nodem. Na Docker Desktop używam host.docker.internal, a na Linuksie po prostu wpinam aplikację do tej samej sieci Compose i odwołuję się do niej po nazwie serwisu. To jest chyba najczęstszy powód pytania “dlaczego po przejściu na Grid wszystkie testy widzą pustą stronę”.

Równoległość ustawiam już po stronie NUnita, plikiem AssemblyInfo.cs:

[assembly: Parallelizable(ParallelScope.Fixtures)]
[assembly: LevelOfParallelism(3)]

Liczba w LevelOfParallelism musi się mieścić w tym, co realnie stoi na Gridzie. Trzy nody po dwie instancje dają sześć slotów, więc trójka wchodzi z zapasem. Jeżeli poproszę o więcej sesji, niż Grid ma slotów, nadmiarowe żądania będą czekać w kolejce huba i najpewniej skończą się timeoutem.

Czego już można dotknąć w becie Grida 4

Bardzo krótki podgląd, żeby wiedzieć, co przyjdzie. Obrazy bety mają własne wydanie z 15 lutego, więc tag przypinam tak samo jak przy trójce:

version: "3.8"

services:
  selenium-hub:
    image: selenium/hub:4.0.0-beta-1-20210215
    ports:
      - "4442:4442"
      - "4443:4443"
      - "4445:4444"

  chrome:
    image: selenium/node-chrome:4.0.0-beta-1-20210215
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
      SE_EVENT_BUS_PUBLISH_PORT: 4442
      SE_EVENT_BUS_SUBSCRIBE_PORT: 4443

Układ z zewnątrz wygląda znajomo: nadal jest hub i nadal jest node. Nowe są dwa porty obok 4444 w kontenerze. To szyna zdarzeń, którą node i hub wymieniają się informacjami o rejestracji i o stanie sesji. Zamiast HUB_HOST node dostaje SE_EVENT_BUS_HOST. Router bety publikuję na hoście jako 4445:4444, żeby nie zająć 4444, na którym stoi Grid 3.

Klient też jest inny. Do bety biorę Selenium.WebDriver w wersji 4.0.0-beta1, w osobnym projekcie testowym, i nie próbuję dzielić kodu z projektem produkcyjnym. Endpoint w becie to http://localhost:4445/, a protokół jest wyłącznie W3C.

I na tym na razie kończę zabawę. Sprawdziłem, że mój zestaw da się uruchomić po drobnych poprawkach, zapisałem sobie listę różnic i wracam do trójki. Wrócę do tematu, kiedy będzie stabilne wydanie.

Typowe wywrotki

Cztery rzeczy, na których straciłem najwięcej czasu.

Brak pamięci współdzielonej. Test wywala się w połowie z komunikatem o zamkniętej sesji albo o padniętej karcie, a w docker-compose logs chrome widać, że Chrome się przewrócił. Prawie zawsze odpowiedzią jest brakujący shm_size: 2gb. To pierwsza rzecz, którą sprawdzam, i pierwsza, o którą pytam, kiedy ktoś zgłasza migające testy na Gridzie.

Mieszanie klienta z serwerem. Selenium.WebDriver 3.141.0 wystrzelony w Grid 4 beta albo klient bety wystrzelony w Grida 3 daje błędy, które wyglądają jak problem sieciowy, a są niezgodnością protokołu. Klient i serwer trzymam w tej samej linii, bez wyjątków.

Tag latest. Obrazy Selenium są odświeżane razem z przeglądarkami, więc latest potrafi przenieść mnie z Chrome 89 na kolejną wersję w środku tygodnia i wywrócić selektory albo zachowanie okna. Ta sama zasada, co przy bazie danych: przypinam pełny tag z datą i podnoszę go świadomie.

localhost w złym miejscu. Raz w HUB_HOST po stronie noda, raz w adresie aplikacji po stronie testu. Za każdym razem chodzi o to samo nieporozumienie: localhost to teraz trzy różne maszyny, w zależności od tego, kto o niego pyta.

Grid nie naprawia testów. Zamienia tylko czterdzieści minut czekania na kilkanaście i zdejmuje przeglądarkę z mojego pulpitu. Jeżeli zestaw był niestabilny szeregowo, po zrównolegleniu będzie niestabilny szybciej - a to, czy w ogóle testuję właściwe rzeczy, to nadal pytanie o cały proces testowy, a nie o infrastrukturę.