BDD i Behat. Testowanie Aplikacji

programit.pl 2 lat temu

Behat jest popularnym narzędziem do testowania pozwalającym na stosowanie Behavior-driven development (BDD).
Czym jest BDD? w uproszczeniu to proces wytwarzania oprogramowania, który wywodzi się z test-driven development (TDD), domain-driven design oraz testowania akceptacyjnego.
Skupia się na współpracy biznesu oraz zespołu wytwarzającego oprogramowanie. W procesie tym opisujemy scenariusze testowe w formie historyjek zrozumiałych dla obu stron. Używa się do tego składni języka naturalnego, łączonej z językiem specyficznym dla danej domeny.

Do pisania testów akceptacyjnych używamy "historyjek użytkowników" (User stories), których struktura wygląda następująco: "jako [rola] chcę [opis funkcji], aby [opis korzyści]". Poszczególne scenariusze piszemy, używając trzech słów kluczowych: Given (stan początkowy), When (opis zdarzenia), Then (potwierdzenie oczekiwanego rezultatu).

W tym wpisie skupię się na tym, jak zainstalować niezbędne narzędzia i jak je skonfigurować, aby można było rozpocząć testowanie interfejsu użytkownika z wykorzystaniem przeglądarki.

Instalujemy zależności

composer require behat/behat --dev

Instalujemy Behat'a


composer require friends-of-behat/mink-extension --dev

Mink jest narzędziem umożliwiającym symulację interakcji pomiędzy przeglądarką a naszymi testami. Jest to kontroler/emulator przeglądarki.
MinkExtension to warstwa integrująca Behat'a i Mink'a. Instalując to narzędzie mamy dostęp do klasy MinkContext. Daje nam ona zaimplementowane podstawowe kroki, które możemy wykorzystać do interakcji z przeglądarką.


composer require dmore/behat-chrome-extension --dev

To narzędzie umożliwia kontrolę przeglądarki bez Selenium. Komunikuję się z Chrome poprzez HTTP i WebSockets, przez co działa znacznie szybciej niż Chrome z Selenium. Obsługuje tryb "headless", za pomocą którego nie ma konieczności instalowania serwera wyświetlania.


composer require friends-of-behat/symfony-extension --dev

SymfonyExtension umożliwia integrację Behata z aplikacją opartą o framework Symfony. Umożliwia definiowanie kontekstów jako serwisów.

Po zainstalowaniu tego dodatku utworzone zostaną następujące pliki:

|config/ | - services_test.yaml |features/ | - demo.feature |tests/ | - Behat/ | - DemoContext.php |behat.yml.dist
composer require phpunit --dev

Domyślnie, SymfonyExtension próbuje załadować plik bootstrap.php, który nie istnieje w aplikacji Symfony. Instalując PHPUnit, plik ten zostanie stworzony.


Do uruchomienia testów będziemy potrzebować przeglądarki. U mnie będzie to Chrome. Poniżej wklejam fragment pliku Dockerfile, który instaluje tą przeglądarkę.

# Install chromedriver RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - RUN bash -c "echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' >> /etc/apt/sources.list.d/google-chrome.list" RUN apt-get update && apt-get -y install google-chrome-stable

Konfigurujemy narzędzia

Behata konfigurujemy w pliku behat.yml. W moim przypadku wygląda on tak:

default: suites: ui_posts: contexts: - App\Behat\Context\PostsContext - App\Behat\Context\FixturesContext filters: tags: "@posts" extensions: FriendsOfBehat\SymfonyExtension: bootstrap: tests/bootstrap.php DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension: ~ Behat\MinkExtension: browser_name: chrome base_url: http://localhost sessions: default: chrome: api_url: "http://localhost:9222"

Skonfigurowałem tu jeden zestaw testowy, który będzie testował prostą CRUDową aplikację do zarządzania postami.
PostsContext w tym przypadku rozszerza wyżej wspomniany Behat\MinkExtension\Context\MinkContext. Dzięki temu nie ma konieczności pisania podstawowych kroków od nowa. jeżeli będziemy posiadać więcej zestawów testowych, dobrze oznaczyć je tagiem (w tym przypadku @posts), aby w kontekście danego zestawu uruchamiały się tylko testy z danym tagiem. Inaczej uruchamiane będą wszystkie zestawy.

W sekcji extensions konfigurujemy dodatki. Między innymi SymphonyExtension, gdzie podajemy ścieżkę do pliku konfiguracyjnego.
ChromeExtension zawiera domyślną konfigurację. Więcej o możliwościach konfiguracyjnych tego narzędzia znajdziemy w dokumentacji.

Piszemy pierwszy scenariusz testowy

Scenariusze testowe piszemy w Gherkin'ie - języku zrozumiałym dla biznesu.
W przypadku naszego prostego CRUD'a chcemy przetestować funkcjonalność wyświetlania, dodawania, edycji i usuwania postów.
Do tego celu należy stworzyć plik posts.feature w katalogu features.

W BDD nie ma formalnych wymagań dotyczących tego jak mają być zapisywane historyjki. Jest jednak pewien standard, który ma szerokie zastosowanie. Na początku dodajemy sekcję Feature, w której nazywamy naszą funkcjonalność. Po niej opisujemy funkcjonalność oraz warość biznesową jako daje. Możemy to zrobić w trzech liniach: "In order to...", "As an...", "I want to".

@posts Feature: Managing posts In order to manage posts As a writer I want to add, edit, delete and display blog posts

Po tej sekcji możemy dodawać scenariusze testowe. Pierwszym u mnie będzie możliwość przejścia na stronę dodawania postu z listingu.

Scenario: Writer goes to create post page Given I am on "/post/" When I follow "Create new" Then I should be on "/post/new"

Jak widzimy, nasz scenariusz napisany jest językiem naturalnym, zrozumiałym dla człowieka. Co zrobić, aby Behat zrozumiał, co chcemy wytestować. Do tego celu potrzebujemy kontekstu. W sekcji wyżej, gdzie pokazywałem plik konfiguracyjny, można zauważyć na początku odniesienie do klasy App\Behat\Context\PostsContext. Konteksty służą do tłumaczenia języka naturalnego na język zrozumiały dla Behat'a.
W przypadku tego testu rozszerzyłem tą klasę o MinkContext, która posiada implementację poszczególnych kroków z powyższego scenariusza, jak i wiele innych.

class PostsContext extends MinkContext { }

W klasie MinkContext znajdziemy metodę:

/** * @Given /^(?:|I )am on "(?P<page>[^"]+)"$/ * @When /^(?:|I )go to "(?P<page>[^"]+)"$/ */ public function visit($page) { $this->visitPath($page); }

Gdy w pliku posts.feature w scenariuszu napiszemy Given I am on "/post/", Behat znajdzie i wywoła tę metodę na podstawie zawartych adnotacji.

Napiszmy teraz kolejny scenariusz, który doda post i sprawdzi, czy wyświetli się na listingu.

Scenario: Writer wants to add a new post and see it on posts page Given I am on "/post/new" When I fill in "Title" with "An example post" And I fill in "Content" with "Lorem ipsum" And I press "Save" Then I should be on "/post/" And On the posts list I can see post with title "An example post" 1 time

W tym scenariuszu dodajemy nowy post poprzez wypełnienie formularza. Zapisujemy go i sprawdzamy, czy na znajduje się na listingu.
Ostatniego przypadku, sprawdzającego ile razy znajdziemy tytuł na stronie, nie znajdziemy w MinkExtension i musimy napisać ją sami.
Może wyglądać tak:

/** * @Then /^On the posts list I can see post with title "([^"]*)" (\d+) times$/ * @Then /^On the posts list I can see post with title "([^"]*)" (\d+) time$/ * @throws ExpectationException */ public function onThePostsListICanSeeTitleNTimes(string $title, int $count): void { $element = $this->getSession()->getPage(); $result = $element->findAll('xpath', "//*[contains(text(), '$title')]"); $resultCount = \count($result); if ($resultCount == $count && str_contains(reset($result)->getText(), $title)) { return; } throw new ExpectationException(sprintf('"%s" was expected to appear %d times, got %d', $title, $count, $resultCount ), $this->getSession()); }

Czasami może zajść konieczność zgrupowania kilku kroków w jeden, np. w sytuacji, gdy dostęp do naszej aplikacji wymaga uwierzytelniania, wymagającego wejścia na stronę logowania, podania loginu i hasła oraz wysłaniu formularza. Możemy to zrobić w ten sposób:

/** * @Given I am logged in as admin */ public function iAmLoggedInAsAdmin(): void { $this->visit('/login'); $this->fillField('email', 'admin@admin'); $this->fillField('password', 'admin1'); $this->pressButton('Sign in'); }

Powyższy krok możemy dodać przed wykonaniem każdego scenariusza testowego. W tym celu w pliku posts.feature dodajemy przed pierwszym scenariuszem sekcję Background:

Background: Given I am logged in as admin

Cały plik posts.feature wygląda teraz tak:

@posts Feature: Managing posts In order to manage posts As a writer I want to add, edit, delete and display blog posts Background: Given I am logged in as admin Scenario: I want to go to create post page Given I am on "/post/" When I follow "Create new" Then I should be on "/post/new" Scenario: Writer wants to add a new post and see it on posts page Given I am on "/post/new" When I fill in "Title" with "An example post" And I fill in "Content" with "Lorem ipsum" And I press "Save" Then I should be on "/post/" And On the posts list I can see post with title "An example post" 1 time

Plik PostsContext wygląda tak:

class PostsContext extends MinkContext { /** * @Given I am logged in as admin */ public function iAmLoggedInAsAdmin(): void { $this->visit('/login'); $this->fillField('email', 'admin@admin'); $this->fillField('password', 'admin1'); $this->pressButton('Sign in'); } /** * @Then /^On the posts list I can see post with title "([^"]*)" (\d+) times$/ * @Then /^On the posts list I can see post with title "([^"]*)" (\d+) time$/ * @throws ExpectationException */ public function onThePostsListICanSeeTitleNTimes(string $title, int $count): void { $element = $this->getSession()->getPage(); $result = $element->findAll('xpath', "//*[contains(text(), '$title')]"); $resultCount = \count($result); if ($resultCount == $count && str_contains(reset($result)->getText(), $title)) { return; } throw new ExpectationException(sprintf('"%s" was expected to appear %d times, got %d', $title, $count, $resultCount ), $this->getSession()); } }

Dodajemy dane testowe

Przed wykonaniem każdego scenariusza chcemy się zalogować. Może się jednak okazać, iż baza użytkowników jest pusta.

Co więcej, po każdym uruchomieniu testów do bazy dodawałby się kolejny post z tym samym tytułem i treścią. To spowoduje, iż już za drugim razem test nie przejdzie.

Dobrze w tym celu wyczyścić bazę danych i dodać dane testowe (Fixturesy) przed uruchomieniem każdego scenariusza. O tym, jak używać fixturesów w Symfony pisałem w tym poście: Fixturesy W Testach Aplikacji Symfony.

Aby załadować dane testowy, stwórzmy sobie FixturesContext, który będzie odpowiedzialny za to zadanie.
U mnie wygląda on tak:

class FixturesContext implements Context { public function __construct( private ContainerInterface $container, private EntityManagerInterface $entityManager ) {} /** * @BeforeScenario @fixtures */ public function setupDatabase(): void { $metaData = $this->entityManager->getMetadataFactory()->getAllMetadata(); $schemaTool = new SchemaTool($this->entityManager); $schemaTool->dropDatabase(); if (!empty($metaData)) { $schemaTool->createSchema($metaData); } } /** * @BeforeScenario @fixtures */ public function loadFixtures(): void { $loader = new ContainerAwareLoader($this->container); $loader->loadFromDirectory(__DIR__.'/../../DataFixtures'); $executor = new ORMExecutor($this->entityManager); $executor->execute($loader->getFixtures(), true); } }

W powyższej klasie widzimy dwie metody. Pierwsza odpowiedzialna za usunięcie i stworzenie bazy danych. Druga odpowiedzialna za wczytanie danych. Każda z metod ma adnotację @BeforeScenario, oznaczającą, iż będzie wywoływana przed uruchomieniem każdego scenariusza. Możemy ograniczyć ładowanie fixturesów tylko do tych scenariuszy, gdzie będą one potrzebne, dodając odpowiedni tag (w tym przypadku @fixtures).
Teraz przed każdym scenariuszem z tym tagiem załadowane zostaną przykładowe dane.

Odpalamy testy

Aby uruchomić testy, musimy wykonać dwa kroki. Pierwszym z nich jest uruchomienie przeglądarki. Robimy to dzięki poniższego polecenia:

google-chrome-stable --disable-gpu --headless --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 --no-sandbox

Gdy przeglądarka działa, możemy uruchomić testy. Robimy to wykonując polecenie:

APP_ENV=test vendor/bin/behat

W konsoli powinniśmy zobaczyć wynik naszych testów. Powinien on wyglądać mniej więcej tak:

Podsumowanie

Behat to narzędzie umożliwiające pisanie testów w sposób zrozumiały zarówno dla biznesu, jak i programistów.
Aby zacząć pisać proste testy należy zainstalować kilka paczek oraz stworzyć scenariusze testowe. Wiele kroków jest obsługiwane przez MinkExtension, co z pewnością ułatwia pierwsze podejście do BDD.

Idź do oryginalnego materiału