docker-console codeception

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ć).
     
3. Najlepsze praktyki zespołów programistycznych