Jak automatyzować testy w docker-console
Jeżeli czytałeś nasze poprzednie wpisy, to już dobrze wiesz jak uruchomić projekt w docker-console. Jeżeli jeszcze tego nie zrobiłeś, to koniecznie zacznij od tego artykułu, gdyż w tym wpisie zakładamy, że masz już uruchomiony projekt z docker-console na swoim komputerze i wszystkie komendy jakie będą wykonywane poniżej, będą się do niego odnosić. W tym artykule chcemy wprowadzić Cię w świat testów automatycznych, z wykorzystaniem Codeception, bazując właśnie na takim projekcie. Oczywiście nie wszyscy muszą automatyzować testy na swoich projektach, lecz jeżeli nie będzie to wymagało dużego nakładu pracy, to zapewne więcej osób przychylnym okiem spojrzy chociażby na zestaw “smoke testów”.
W Droptica staramy się być najlepszą drupalową agencją na rynku i głęboko wierzymy, że automatyczne testy pomagają nam to osiągnąć.
Czym jest codeception?
Codeception jest frameworkiem przeznaczonym do tworzenia testów jednostkowych, akceptacyjnych i funkcjonalnych. Co prawda opiera się on na języku PHP, jednak do rozpoczęcia z nim pracy potrzebna jest wiedza wyłącznie na poziomie podstawowym z uwagi na zestaw własnych komend, które Codeception oferuje (oczywiście im więcej będziesz chciał przetestować tym więcej o PHP i strukturze testów automatycznych powinieneś się nauczyć). Testy w Codeception są pisane w stylu BDD (Behavior Driven Development), czyli zbiorze krótkich historyjek, które mówią nam o tym, jak powinien zachować się system, kiedy coś konkretnego się wydarzy.
Instalacja
Mamy przygotowany gotowy obraz z Codeception, dlatego nie będziecie musieli niczego instalować. Po prostu wystarczy pobrać pliki do naszego projektu (oczywiście mam tu na myśli projekt utworzony na podstawie artykułu o podstawach docker-console). W tym celu w katalogu projektu wystarczy wywołać komendę:
docker-console init-tests --tpl drupal7
lub w przypadku projektu z Drupal 8
docker-console init-tests --tpl drupal8
Stworzy nam ona katalog tests, w którym będziemy mieli zainstalowane i gotowe do użycia środowisko do pisania testów. Pozostała konfiguracja będzie taka sama dla Drupala 7 i 8 (nie licząc modułów dodatkowych napisanych dla konkretnej wersji).
Konfiguracja ogólna
Ogólne dane konfiguracyjne przechowywane są w pliku codeception.yml, który powinien wyglądać mniej więcej tak, jak ten pokazany na obrazku niżej. Na jego podstawie możemy skonfigurować ścieżki do projektu, zwiększyć limit pamięci, ustawić domyślną konfigurację dla danego modułu itp. Na tę chwilę do sprawnego działania projektu nic nie trzeba tutaj ustawiać.
Konfiguracja szczegółowa
Dodatkowo poza głównym plikiem konfiguracyjnym każdy zestaw testów (suite) posiada osobny plik konfiguracyjny np. acceptance.suite.yml. Tworzymy tu klasę testera, która określa nam dokładnie z jakich modułów możemy korzystać podczas wykonywania testu i w razie konieczności pozwala nadpisać ustawienia z pliku ogólnego. Tu także nie musimy niczego zmieniać w tej chwili. Jednak chcę zwrócić Waszą uwagę na ustawienia PhpBrowser. Ustawiony tutaj url (http://web) nie musi być zmieniany, ponieważ nazwa web odwołuje się do nazwy kontenera dockerowego, w którym znajduje się nasza strona. Zmienna auth odwołuje się natomiast do użytkownika i hasła, którym możemy zabezpieczyć dostęp do naszej strony, używając .htaccess. Ponieważ na tym projekcie nie mamy takiego zabezpieczenia, możemy spokojnie usunąć tę linijkę z pliku konfiguracyjnego. Omówienie pozostałych modułów jest kwestią bardziej rozbudowaną, którą postaram się przybliżyć w następnym artykule.
Krótki wstęp do testów
Jak już wcześniej pisałem, z pomocą Codeception można pisać testy jednostkowe, funkcjonalne i akceptacyjne.
Te ostatnie w naszym przypadku podzieliliśmy w zależności od użytego drivera. Główną przyczyną takiego podziału był krótszy czas wykonywania testów w przypadku zastosowania PhpBrowser oraz konieczność zastosowania WebDrivera w przypadku testów, gdy na stronie wywoływane były akcje z użyciem JavaScriptu.
PhpBrowser | WebDriver | |
---|---|---|
Silnik przeglądarki | Guzzle + Symfony BrowserKit |
Chrome lub Firefox |
JavaScript | NIE | TAK |
Widoczność elementów na stronie |
Tekst jest widoczny jeżeli znajduje się w źródle strony |
Tekst jest widoczny jeżeli jest aktualnie widoczny dla użytkownika na stronie |
Czytanie nagłówka z odpowiedzi HTTP |
TAK | NIE |
Szybkość testów | Średnia | Niska |
W tabelce poniżej zaprezentowano ogólne różnice pomiędzy rodzajami testów. Zostały tu także dodane testy JS_capable, które są testami akceptacyjnymi. Jednak w odróżnieniu od zestawu acceptance, wykonywanego za pomocą PhpBrowser, testy JS_cablable uruchamiane są za pomocą Webdrivera w przeglądarce Chrome lub Firefox (tak jak pisałem wcześniej jest to nasza konfiguracja i oczywiście nic nie stoi na przeszkodzie, żebyś w swoim projekcie włączył WebDriver w suite acceptance).
Unit | Functional | Acceptance | JS_capable | |
---|---|---|---|---|
Zakres | klasa PHP | PHP Freamework | Strona uruchomiona w przeglądarce (widziany HTML) |
Strona widziana tak jak docelowy użytkownik |
JavaScript | NIE | NIE | NIE | TAK |
Wymagany webserwer |
NIE | NIE | TAK | TAK |
Szybkość testów | Wysoka | Wysoka | Średnia | Niska |
Plik konfiguracyjny | unit.suite.yml | functional.suite.yml | acceptance.suite.yml | js_capable.suite.yml |
Selektory
Ostatnią rzeczą jaką chciałbym omówić, zanim przystąpimy do pisania przykładowych testów, są lokalizatory, czyli jak dokładnie Codeception znajduje elementy na których chcemy działać podczas testu. W Codeception elementy możemy znajdować za pomocą:
- id np. ‘#test’ co odpowiada
<div id=”test”>
- klas np ‘.test’ co odpowiada
<div class=”test”>
- nazwy np. ‘test’ co odpowiada
<div name=”test”>
lub<input value=”test”>
lub też po prostu tekstowi widocznemu na stronie - css np. 'input[value=test ]' co odpowiada
<input value="test">
- xpath np. //input[@type='submit'][contains(@value, 'test')]"] co odpowiada
<input type="submit" value="test">
Możemy też użyć trochę bardziej skomplikowanych lokalizatorów, które są opisane tutaj: http://codeception.com/docs/reference/Locator
Niezależnie od tego jak będziemy lokalizować nasz element, powinniśmy starać się, żeby zrobić to w sposób jak najbardziej jednoznaczny.
Testy akceptacyjne
Jeśli masz przypadki testowe, które zawsze musisz wyklikać przed wdrożeniem strony, aby potwierdzić, że działa ona poprawnie to najprawdopodobniej masz też doskonałych kandydatów do zautomatyzowania swojej pracy i umieszczenia ich w folderze testów akceptacyjnych Codeception. W naszej konfiguracji w katalogu acceptance umieszczamy te testy, które da się sprawdzić bez użycia JavaScriptu, gdyż w tym przypadku używamy PhpBrowsera (nie jest to konieczne, ale z pewnością szybsze rozwiązanie). W przypadku tych testów mamy do dyspozycji wiele pomocnych i już zdefiniowanych funkcji, których listę i opis znajdziesz na stronie: http://codeception.com/docs/modules/PhpBrowser
Jako przykład widzimy tutaj test, w którym logujemy się na konto administratora i sprawdzamy czy widzi on na stronie głównej tekst jednego z artykułów.
<?php
class ExampleAcceptanceTestCest
{
public function _before(AcceptanceTester $I) {
}
public function _after(AcceptanceTester $I) {
}
/** TESTS */
/**
* @param \AcceptanceTester $I
*
*/
public function exampleTest(AcceptanceTester $I) {
$I->wantTo('Test - I can log in as admin and see article');
$I->amOnPage('/');
$I->fillField('#edit-name', 'admin');
$I->fillField('#edit-pass', 'admin');
$I->click('Log in');
$I->amOnPage('/');
$I->see('Droptica sięga korzeniami roku 2008, kiedy to jeden ze współzałożycieli stworzył swoją pierwszą firmę programistyczną openBIT');
$I->amOnPage('/user/logout');
}
}
Testy js_capable
Dodatkowo dla naszych potrzeb dodaliśmy suite js_capable, w którym piszemy testy akceptacyjne wymagające JavaScript. Zasadniczo więc możemy skopiować test z katalogu acceptance, wkleić go do js_capable i już powinien działać. Musimy jednak pamiętać, że w tym przypadku elementy są dla programu widoczne tak, jak dla każdego innego użytkownika, a więc np. dodanie w css display:none wyłączy nam ten element z pola widzenia (przy PhpBrowser taki element będzie widoczny, ponieważ znajduje się w kodzie HTML). Dla porównania możesz prześledzić listę funkcji dostępnych dla WebDrivera: http://codeception.com/docs/modules/WebDriver
Przed pokazaniem przykładowego testu, chciałbym tu jeszcze omówić konfigurację tego typu testów. Otóż do ich odpalenia nie potrzebujemy lokalnie posiadać WebDrivera ani nawet przeglądarki, gdyż wszystko to znajduje się w kontenerze dockera. Od razu po zainicjowaniu projektu z testami środowisko jest gotowe do uruchomienia testów w przeglądarce Chrome z najnowszym Webdriver. Jeśli natomiast chciałbyś sprawdzić jak Twoje testy radzą sobie w Firefoxie, musisz to zmienić w dwóch miejscach. Po pierwsze w pliku js_capable.suite.yml chrome zamienić na firefox.
Drugą sprawą jest zmiana obrazu z selenium na taki, na którym znajduje się Firefox. Można to zrobić edytując plik dc_settings.py., znajdujący się w katalogu docker_console. Należy w nim zmienić obraz standalone-chrome na standalone-firefox.
Więcej o tych obrazach możesz dowiedzieć się na stronach:
https://hub.docker.com/r/selenium/standalone-chrome/
https://hub.docker.com/r/selenium/standalone-firefox/
Teraz pora na przykład, który dodaje nody typu article i basic page.
<?php
class JSCentreTestsCest
{
public function _before(JSCapableTester $I) {
}
public function _after(JSCapableTester $I) {
}
/** TESTS */
/**
* @param \JSCapableTester $I
*
*/
public function addArticle(JSCapableTester $I) {
$I->wantTo('Test - I add article');
$I->amOnPage('/');
$I->fillField('#edit-name', 'admin');
$I->fillField('#edit-pass', 'admin');
$I->click('Log in');
$I->amOnPage('/node/add/article');
$I->fillField('#edit-title', 'Test article');
$I->fillField('#edit-body-und-0-value', 'Test text in body article');
$I->click('#edit-submit');
$I->see('Article Test article has been created.');
$I->see('Test text in body article');
$I->amOnPage('/user/logout');
}
/**
* @param \JSCapableTester $I
*
*/
public function addPage(JSCapableTester $I) {
$I->wantTo('Test - I add basic page');
$I->amOnPage('/');
$I->fillField('#edit-name', 'admin');
$I->fillField('#edit-pass', 'admin');
$I->click('Log in');
$I->amOnPage('/node/add/page');
$I->fillField('#edit-title', 'Test basic page');
$I->fillField('#edit-body-und-0-value', 'Test text in body page');
$I->click('#edit-submit');
$I->see('Basic page Test basic page has been created.');
$I->see('Test text in body page');
$I->amOnPage('/user/logout');
}
}
Jeżeli chcesz się dowiedzieć czegoś więcej o testach akceptacyjnych, koniecznie odwiedź stronę: http://codeception.com/docs/03-AcceptanceTests
Testy funkcjonalne
Testy funkcjonalne piszemy w sposób bardzo zbliżony do tego, w jaki pisaliśmy testy akceptacyjne. Główna różnica polega tu na tym, iż testy te nie muszą być odpalane na webserverze, przez co są znacznie szybsze. Dodatkową zaletą są dostępne dla nich dodatkowe polecenia, pozwalające testować frameworki takie jak Symfony, Laravel5,
Yii2, Yii, Zend Framework 2, Zend Framework 1.x i Phalcon. W zasadzie tylko jeżeli w projekcie używasz któregoś z tych frameworków, pisanie testów funkcjonalnych ma sens (chyba że sam dopiszesz sobie odpowiednie funkcje).
Poniżej przedstawię prosty przykład jak takie testy mogą wyglądać.
Zanim zaczniemy pisać test musimy w pliku functional.suite.yml dodać możliwość korzystania z modułu Db dla testów funkcjonalnych. Po tej zmianie plik powinien wyglądać w następujący sposób:
Teraz napiszemy test, który sprawdzi, czy w bazie danych znajdują się nody (dodawanie nodów też mogłoby być w tym teście).
<?php
use Drupal\Pages\Page;
use Codeception\Util\Shared\Asserts;
class ExampleFunctionalTestCest
{
use Asserts;
public function _before(FunctionalTester$I) {
}
public function _after(FunctionalTester $I) {
}
/** TESTS */
/**
* @param \FunctionalTester $I
*/
public function exampleTestOfText(FunctionalTester $I) {
$I->wantTo('Test - I can see node in database');
$I->haveInDatabase('node', array('type' => 'article', 'title' => 'WSZYSTKO, CO CHCIELIBYŚCIE WIEDZIEĆ O REKRUTACJI W DROPTICE'));
$I->haveInDatabase('node', array('type' => 'page', 'title' => 'Test page'));
}
}
Więcej o testach funkcjonalnych możecie przeczytać tutaj: http://codeception.com/docs/04-FunctionalTests
Testy jednostkowe
Jeżeli pisałeś wcześniej testy jednostkowe PHPUnit, to w zasadzie nie musisz się uczyć niczego od nowa i możesz spokojnie używać tej samej składni, której używałeś wcześniej.
W moim przykładzie do poprawnego działania testu konieczne będzie umożliwienie Codeception korzystanie z poleceń Drupala. W tym celu w pliku unit.suite.yml należy odblokować moduł. Gdy to zrobimy nasz plik powinien wyglądać jak ten poniżej:
Teraz możemy przystąpić do napisania testu, który będzie sprawdzać, czy mamy w Drupalu włączony moduł backup and migrate.
<?php
class ExampleUnitTest extends \Codeception\Test\Unit
{
/**
* @var \UnitTester
*/
protected $tester;
protected function _before()
{
}
protected function _after()
{
}
/** TESTS */
public function testModulesEnabled()
{
$modules[] = 'backup_migrate';
foreach ($modules as $modules_name) {
$result = module_exists($modules_name);
$this->assertEquals(TRUE, module_exists($modules_name));
}
}
}
Jeżeli chcesz się dowiedzieć czegoś więcej o testach jednostkowych odwiedź stronę - http://codeception.com/docs/05-UnitTests
Uruchamianie testów
Kiedy mamy już napisane testy, nie pozostaje nam nic innego, jak uruchomić je i sprawdzić jak to działa. Pamiętaj, że przed uruchomieniem testów musisz mieć uruchomione kontenery projektu (polecenie dcon up). Nasze testy możemy uruchomić na kilka sposobów:
- wszystkie napisane przez nas testy
dcon test
- tylko dany zestaw testów, np.
dcon test acceptance
- pojedynczy plik z testami, np.
dcon test acceptance/ExampleFilet.php
- pojedynczy test, np.
dcon test acceptance/ExampleFile.php::ExampleTest
Po uruchomieniu testów poleceniem dcon test powinieneś w swojej konsoli widzieć, jak wykonują się testy, a na koniec zobaczyć coś podobnego do tego z obrazka zamieszczonego poniżej.
Raporty
Oczywiście Codeception nie pozostawia nas jedynie z tym co zobaczymy w konsoli podczas wykonania testu. Po zakończeniu testów w folderze _output generowany jest raport w formacie XML oraz HTML. Jeżeli naciśniemy w nim na plus, znajdujący się obok konkretnego testu, zobaczymy jakie kroki zostały wykonane w ramach danego testu.
Teraz postarajmy się zmienić coś na stronie lub w naszych testach tak, żeby wywołać błąd. Jak widzimy na poniższym obrazku, w przypadku błędu mamy dokładnie zaznaczony krok, w którym nastąpił błąd. Dodatkowo, ponieważ był to test wykonywany w przeglądarce z użyciem WebDrivera, dostępny mamy screen oraz krótki opis tego, co się stało. W przypadku kiedy zawiedzie którykolwiek z testów akceptacyjnych, dostępny mamy plik HTML z kodem strony z momentu wystąpienia błędu.
Pliki projektu
Przykłady opisane w tym artykule możesz uruchomić u siebie również poprzez pobranie ich bezpośrednio z repozytorium projektu i zmienienie brancha na codeception-start.
Repozytorium projektu:
https://github.com/DropticaExamples/docker-console-project-example
Zrzut bazy danych:
https://www.dropbox.com/s/tcfkgpg2ume17r3/database.sql.tar.gz?dl=0
Pliki projektu:
https://www.dropbox.com/s/hl506wciwj60fds/files.tar.gz?dl=0
Zakończenie
Mam nadzieje, że po przeczytaniu tego tekstu, będziecie wiedzieć, jak rozpocząć swoją przygodę z Codeception i że chodź trochę Was do tego zachęciłem. Na koniec postaram się napisać kilka wskazówek, których powinniście trzymać się pisząc swoje testy:
- zawsze pamiętaj o asercjach w testach, gdyż test który nic nie sprawdza, to nie test,
- staraj się pisać testy stabilne (czyli dobieraj selektorów elementów tak, żeby test mógł je zawsze jednoznacznie zlokalizować) - nikt nie chce fałszywie negatywnych czy też fałszywie pozytywnych rezultatów,
- pisz raczej kilka mniejszych testów niż jeden duży (jeżeli się coś zepsuje, kolejne testy będą miały szansę dalej sprawdzać stronę),
- staraj się pisać testy niezależne od siebie (a jeśli to niemożliwe, powiedz systemowi np. poprzez adnotację @depends, żeby w przypadku niepowodzenia w teście pominął kolejny),
- nie automatyzuj wszystkiego na siłę (o tym czy coś zautomatyzować czy nie można napisać kolejny artykuł, o ile nie książkę - ja pokuszę się tylko o stwierdzenie, aby przed napisaniem każdego testu zastanowić się, czy naprawdę tego potrzebujemy),
- jeżeli używasz jakiegoś kodu więcej niż raz, pomyśl o wyciągnięciu go do osobnej funkcji pomocniczej,
- staraj się nie pisać selektorów i urli na sztywno w testach ( używaj Page Object Pattern - o tym też postaramy się wkrótce napisać).