codeception-drupal

Jak zaprzyjaźniliśmy Codeception z Drupalem

Drupal development to nasz chleb powszedni, a z uwagi na to, że będąc drupalową agencją opieramy swoje projekty głównie na tej platformie, testy powinny w sposób naturalny właśnie z takimi projektami najlepiej współpracować. Dlatego też w ramach tej współpracy postanowiliśmy uzupełnić standardową funkcjonalność Codeception o kilka udogodnień dedykowanych właśnie dla Drupala.

Tak jak przy poprzednim wpisie, przedstawione tutaj przykłady będą bazowały na projekcie działającym z docker-console. Dlatego też zachęcamy do zapoznania się w pierwszej kolejności z poprzednimi wpisami na blogu. Jeśli jednak masz własny projekt Codeception i chcesz go tylko zmodyfikować tak żeby lepiej współpracował z Drupalem ten artykuł jest również dla Ciebie.

Przystępując do rozszerzania funkcjonalności naszych testów, mieliśmy wymagania, których nie spełniały istniejące już moduły, dlatego też większość przedstawionych tu rozwiązań jest naszego autorstwa. Bazują one na innych rozwiązaniach dostępnych już wcześniej, ale mam nadzieję, że spodoba się Wam to, jak Droptica je ulepszyła i połączyła w całość dostępną dosłownie od razu w naszej konfiguracji projektu Codeception.

codeception-drupal-bootstrap

Pierwszym przedstawionym tu modułem będzie codeception-drupal-bootstrap, który możecie znaleźć pod adresem https://github.com/droptica/codeception-drupal-bootstrap.
Jest to fork z modułu: https://github.com/Chapabu/codeception-module-drupal (został przez nas przerobiony, ponieważ nie pozwalał on oryginalnie na używanie go w wielu suite jednocześnie oraz nie posiadał wersji dla Drupala 8). Pozwala on na używanie funkcji drupalowych w testach i w innych modułach codeception. 

Konfiguracja

Aby móc używać tego modułu należy go skonfigurować w danym pliku yaml dla danego suite np. unit.suite.yml

Odblokowanie w konfiguracji suite dla Drupala 7
 

        - \Codeception\Module\Drupal7\Drupal7:
            root: '/app/app'
            relative: no

Odblokowanie konfiguracji dla Drupala 8:

        - \Codeception\Module\Drupal8\Drupal8:
            root: '/app/app/'
            env: 'prod'
            relative: no
            site_dir: 'default'

Przykład

Jego zastosowanie dla osób pracujących z Drupalem jest niemal oczywiste. Już w poprzednim wpisie pokazaliśmy przykład jak dzięki użyciu poleceń z Drupala można sprawdzić, czy dany moduł jest włączony. W tym wpisie pokażemy kolejny pomocny test, który będzie sprawdzał czy features na Waszej stronie nie jest nadpisany. Jeśli chcesz, żeby ten przykład działał na Twoim projekcie, musisz dodać moduł features do projektu i wyeksportować jakiś features lub po prostu pobierz pliki z brancha codeception-drupal gdzie zrobiliśmy to za Ciebie.

<?php

class ExampleUnitTest extends \Codeception\Test\Unit
{
    /**
     * @var \UnitTester
     */
    protected $tester;

    protected function _before()
    {
    }

    protected function _after()
    {
    }

    /**   TESTS     */
     
    public function testFeaturesDefault() {

      module_load_include('inc', 'features', 'features.export');

      // Load information about feature.
      $features[] = 'test';
      $features_states = features_get_component_states($features, FALSE, TRUE);

      foreach ($features_states as $feature_name => $feature_state) {
        $overridden_features = array_filter($feature_state, function($item) {
          return $item != FEATURES_DEFAULT;
        });
        $this->assertEquals(TRUE, empty($overridden_features), 'Feature ' . $feature_name . ' is overridden.');
      }
    }

}

codeception-drupal-users

Kolejnym przedstawionym tutaj modułem będzie codeception-drupal-users, który możecie znaleźć tutaj: https://github.com/droptica/codeception-drupal-users. Jest to przerobiony moduł https://github.com/ixis/codeception-module-drupal-user-registry.
Służy on do automatycznego tworzenia testowych użytkowników drupalowych w trakcie uruchamiania suite z testami. Za jego pomocą można w łatwy sposób stworzyć i później korzystać z testowych użytkowników o określonych rolach w trakcie wykonywania testów. Domyślnie po zakończeniu wszystkich testów z danego suite, stworzeni użytkownicy zostaną usunięci (chyba, że ustawimy inaczej). 
Oryginalny moduł używa drusha do tworzenia userów. Jednak taki sposób uniemożliwiał wypełnienie customowych pól profilowych. Dlatego też nasz moduł zamiast drusha używa do tworzenia użytkowników funkcji drupalowych. Dzięki temu, oprócz standardowych danych użytkownika mamy możliwość uzupełnienia również customowych pól profilowych.

Konfiguracja

Konfiguracja jest dość łatwa i odbywa się w pliku konfiguracyjnym danego suite. Należy jednak pamiętać o kilku rzeczach. Po pierwsze do prawidłowego działania tego modułu powinniśmy mieć w danym suite włączony moduł codeception-drupal-bootstrap. Dodatkowo na projekcie powinien być włączony moduł Entity API (https://www.drupal.org/project/entity).

Przykłady

Najlepiej konfigurację omówić na przykładzie, dlatego też w swoim projekcie dodaliśmy 2 dodatkowe pola oraz 3 dodatkowe role dla użytkowników (jeżeli nie wiesz jak to zrobić u siebie, zmiany są dostępne na branchu docker-drupal). Po włączeniu modułu musimy określić jakich użytkowników chcemy stworzyć. Dlatego też na początku deklarujemy sobie role jakie chcielibyśmy aby mieli nasi użytkownicy (możemy to zrobić tak, jak pokazano na przykładzie poniżej lub też zadeklarować np. roles_custom, gdzie użytkownik będzie posiadał kilka ról zdefiniowanych w systemie). Po opisaniu ról na projekcie, określam sobie dodatkowo jakimi danymi wypełnić dodane przez nas pola (mógłbym je określić osobno dla każdego użytkownika).  Po tym wszystkim możemy przejść do określenia konkretnie użytkowników jakich chcemy, aby codeception tworzyło nam przed wykonaniem testu. Na koniec powinniśmy w create mieć wpisane true jeżeli chcemy, aby użytkownicy byli tworzeni przed uruchomieniem danego suite. To samo tyczy się delete, z tą tylko różnicą, że w tym przypadku chodzi o usunięcie użytkowników po wykonaniu zestawu testów. W moim przykładzie użytkownicy nie są usuwani, żebyśmy po wykonaniu testu mogli w systemie sprawdzić czy faktycznie zostali oni stworzeni. Jeżeli z taką konfiguracją uruchomimy test ponownie, to nowi użytkownicy nie zostaną zdublowani tylko pojawi się informacja, że dany użytkownik istnieje już w systemie.

        - ManageUsers:
            defaultPass: "123"

#Available roles:
#1 anonymous user
#2 authenticated user
#3 administrator
#4 editor
#5 user
#6 nodoby

            roles_admin: &roles_admin
              2: "authenticated user"
              3: administrator

            roles_editor: &roles_editor
              2: "authenticated user"
              4: editor

            roles_user: &roles_user
              2: "authenticated user"
              5: user

            roles_nodoby: &roles_nodoby
              2: "authenticated user"
              6: nodoby

            custom_fields: &custom_fields
              field_inne_pole: 'Fname'
              field_custom_field: 'key1'

            users:
              admin_user:
                name: admin_user
                email: [email protected]
                roles: *roles_admin
                custom_fields:
                  <<: *custom_fields

              editor_user:
                name: editor_user
                email: [email protected]
                roles: *roles_editor
                custom_fields:
                  field_inne_pole: 'Fname2'
                  field_custom_field: 'key2'

              user_user:
                name: user_user
                email: [email protected]
                roles: *roles_user
                custom_fields:
                  <<: *custom_fields

              nodoby_user:
                name: nodoby_user
                email: [email protected]
                roles: *roles_nodoby
                custom_fields:
                  <<: *custom_fields

            create: true                 # Whether to create all defined test users at the start of the suite.
            delete: false                 # Whether to delete all defined test users at the end of the suite.

Dla Drupala 8 konfiguracja samego modułu wygląda niemal identycznie. Jedyną zmianą jest włączenie modułu w konfiuracji dodając:

- \Codeception\Module\Drupal8\ManageUsers:

zamiast wcześniej użytego ManageUsers.

Oczywiście funkcji napisanych w tym module możesz również używać podczas pisania testu. Dosyć przydatnymi funkcjami są te, które pomagają nam w teście odwołać się do testowego użytkownika lub grupy użytkowników:

  • getAllTestUsers()
  • getTestUsersByRoles($roles = array(), $return_one_user = FALSE)
  • getTestUserByName($username)

codeception-drupal-content-types 

Kolejnym z naszych drupalowych modułów jest codeception-drupal-content-types, który można znaleźć pod adresem: https://github.com/droptica/codeception-drupal-content-types. Powstał on na bazie modułu:https://github.com/ixis/codeception-drupal-content-types. Służy on do tworzenia i usuwania testowych nodów. 

Konfiguracja

W celu włączenia modułu należy do pliku konfiguracyjnego yml dodać poniższe linijki. 

        - DrupalContentTypeRegistry:
            contentTypesAutoDump: true
            contentTypesAutoDumpFile: "contentTypes.yml"
            contentTypesFile: "contentTypes.yml"
            customFieldsFile: "contentTypesOverrides.yml"

Kiedy już to zrobimy kolejne uruchomienie testów powinno wygenerować w naszym projekcie plik contentTypes.yml, w którym to będziemy mieli opisaną strukturę nodów dostępnych w projekcie. Przykład takiego pliku możemy zobaczyć na obrazku poniżej.

Struktura node'ów


Jeżeli w danym typie zawartości mamy zdefiniowane pola typu field collection lub inne, które składają się zagnieżdżonych pól (np. location), wówczas w pliku contentTypes.yml zostaną uwzględnione jedynie te główne pola (bez zagnieżdżonych). Aby możliwe było wypełnienie tych zagnieżdżonych pól np. z field collection, należy je uwzględnić w pliku contentTypesOverrides.yml.  Wszystkie osoby, które zainicjowały projekt przy użyciu komendy docker-console init-tests lub też skopiowały kody z naszego przykładowego repozytorium, powinny już taki plik mieć u siebie.  Powinien on zawierać wskazówki jak nadpisać daną konfigurację i wyglądać jak ten przedstawiony poniżej.

Propozycja testów w przykładowym repozytorium.


Przykład

W naszym przykładzie pokażemy jak za pomocą tego modułu stworzyć noda typu article.

class NewNodeTestCest
{

 /**
   * @var string
   * article_title
   */
  private $article_title;

  /**
   * @var string
   * article_body
   */
  private $article_body;

  /**
   * @var string
   * article_tags
   */
  private $article_tags;

  function __construct() {
    $this->article_title = 'Test article';
    $this->article_body = 'Test article body';
    $this->article_tags = 'Test article tag'; 
  }

  public function _before(AcceptanceTester $I) {
  }

  public function _after(AcceptanceTester $I) {
  }

  /**   TESTS     */

  /**
   * @param \AcceptanceTester $I
   * 
   */
  public function newArticle(AcceptanceTester $I, UserSteps $U) {
    $I->wantTo('Test - I can log in as admin and add and delete article');
    $I->amOnPage('/');
    $user = $I->getTestUserByName('admin_user');
    $U->login($user->name, $user->pass);
     $fields_values = array(
      'title' => $this->article_title,
      'body' => $this->article_body,
      'field_tags' => $this->article_tags
    );
    $I->createNode($I, 'article', $fields_values, NULL, FALSE);
    $nid = $I->grabLastCreatedNid($I);
    $I->deleteNodeFromStorage($nid);
    $I->amOnPage('/user/logout');
  }

 }

codeception-drupal-content-types + NodeSteps

Aby jeszcze uprościć sobie pracę z tym modułem dodaliśmy sobie dodatkowe funkcje pomocnicze, które pozwalają nam określić jakim użytkownikiem chcielibyśmy dodać lub usunąć danego noda.  W pliku NodeSteps znajdują się funkcję, które przed dodaniem/usunięciem noda logują nas na odpowiedniego użytkownika. Przykład użycia tych funkcji zaprezentowaliśmy poniżej dodając noda typu page jako admin_user.
 

<?php

use Step\Acceptance\UserSteps;
use Step\Acceptance\NodeSteps;

class NewNodeAsUserTestCest
{

 /**
   * @var string
   * page_title
   */
  private $page_title;

  /**
   * @var string
   * page_body
   */
  private $page_body;

  function __construct() {
    $this->page_title = 'Test page';
    $this->page_body = 'Test page body';
  }

  public function _before(AcceptanceTester $I) {
  }

  public function _after(AcceptanceTester $I) {
  }

  /**   TESTS     */

  /**
   * @param \AcceptanceTester $I
   * 
   */
  public function newPage(AcceptanceTester $I, UserSteps $U, NodeSteps $N) {
    $I->wantTo('Test - I can add and delete page as admin');
    $I->amOnPage('/');
     $fields_values = array(
      'title' => $this->page_title,
      'body' => $this->page_body
    );
    $nid = $N->createNewNodeAsUser('admin_user', 'article', $fields_values);
    $N->deleteNodeAsUser('admin_user', $nid);
  }

 }

codeception-drupal-storage

Ostatnim przedstawionym tutaj modułem będzie moduł codeception-drupal-storage, który możecie znaleźć tutaj: https://github.com/droptica/codeception-drupal-storage.
Służy on do przechowywania zmiennych oraz stworzonych testowych nodów globalnie w całym zestawie testów, aż do czasu kiedy zakończy się wykonywanie testów z danego suite.
W tym module możemy skorzystać z następujących metod:

  • setVariableToStorage($name, $value)
  • getVariablesFromStorage($names = array())
  • getVariableFromStorage($name)
  • deleteVariableFromStorage($name)
  • appendNodeToStorage($nid)
  • getAllNodesFromStorage()
  • getNodeFromStorage($nid)
  • deleteNodeFromStorage($nid)

Konfiguracja

Moduł nie wymaga skomplikowanej konfiguracji, wystarczy go włączyć dodając do pliku konfiguracyjnego danego suite poniższą linijkę:

- SuiteVariablesStorage

Przykład

Jak w każdym przypadku, tak i w tym najlepiej działanie danego modułu zobaczyć na przykładzie. W tym przedstawionym poniżej, tworzenie noda, sprawdzanie czy node zawiera poprawną treść i usuwanie noda podzieliliśmy na osobne testy, dzięki czemu możemy zaprezentować jak daną wartość przenieść z jednego testu do innego. Oczywiście nasze testy nie muszą znajdować się w jednym pliku. Ważne aby należały do tego samego suite, ponieważ po zakończeniu danego zestawu testów wszystkie wartości w nim stworzone są usuwane (możemy też taką wartość usunąć wcześniej odpowiednia metodą).

<?php

use Step\Acceptance\UserSteps;
use Step\Acceptance\NodeSteps;

class VariablesStorageTestCest
{

 /**
   * @var string
   * page_title
   */
  private $page_title;

  /**
   * @var string
   * page_body
   */
  private $page_body;

  function __construct() {
    $this->page_title = 'Test page';
    $this->page_body = 'Test page body';
  }

  public function _before(AcceptanceTester $I) {
  }

  public function _after(AcceptanceTester $I) {
  }

  /**   TESTS     */

  /**
   * @param \AcceptanceTester $I
   * 
   */
  public function newPage(AcceptanceTester $I, UserSteps $U, NodeSteps $N) {
    $I->wantTo('Test - I can log in as admin and add article');
    $I->amOnPage('/');
     $fields_values = array(
      'title' => $this->page_title,
      'body' => $this->page_body
    );
    $nid = $N->createNewNodeAsUser('admin_user', 'article', $fields_values);
    $I->setVariableToStorage('page_nid', $nid);
  }

  /**
   * @param \AcceptanceTester $I
   * 
   */
  public function seeTextOnPage(AcceptanceTester $I) {
    $I->wantTo('Test - I can see page title and body text on test page');
    $nid = $I-> getVariableFromStorage('page_nid');
    $I->amOnPage('/node/'.$nid);
    $I->canSee($this->page_title);
    $I->canSee($this->page_body);
  }

  /**
   * @param \AcceptanceTester $I
   * 
   */
  public function deletePage(AcceptanceTester $I, UserSteps $U, NodeSteps $N) {
    $I->wantTo('Test - I can delete test page');
    $nid = $I-> getVariableFromStorage('page_nid');
    $N->deleteNodeAsUser('admin_user', $nid);
  }

 }

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-drupal.

Repozytorium projektu:
https://github.com/DropticaExamples/docker-console-project-example
Zrzut bazy danych:
https://www.dropbox.com/s/r0o3u9gjp3dccd4/database.sql.tar.gz?dl=0
Pliki projektu:
https://www.dropbox.com/s/hl506wciwj60fds/files.tar.gz?dl=0

Funkcje pomocnicze

Oprócz dodawania do projektu modułów, każdy z projektów uzupełniamy o specjalne funkcje, które pomagają nam w przetestowaniu strony. Używanie takich funkcji pokazywaliśmy, kiedy prezentowaliśmy działanie NodeSteps z modułem codeception-drupal-content-types. (Są to  specjalne klasy o których możesz przeczytać tutaj http://codeception.com/docs/06-ReusingTestCode#StepObjects. Więcej o nich będziesz mógł przeczytać w jednym z kolejnych naszych artykułów). Funkcje pomocnicze można też pisać w plikach specjalnie do tego przygotowanych do każdego zestawu testów np. plik Acceptance w folderze _support/Helper lub też możecie stworzyć nowy plik i w nim pisać funkcje pomocnicze, jak w naszym przypadku plik DrupalHelper znajdujący się w tym samym folderze. Niezależnie od tego jaki wariant wybierzemy, musimy pamiętać o tym, żeby odblokować dostęp do tych funkcji w danym suite w pliku ylm jak w przykładzie poniżej.

Plik suite - strzałki wskazują odpowiednie linijki

Jeżeli tego nie zrobimy to w każdym pliku, w którym będziemy chcieli użyć danej funkcji musimy je zaimportować na początku pliku dodając np.

use Step\Acceptance\UserSteps;
use Step\Acceptance\NodeSteps;

Zakończenie

Mamy nadzieję, że spodobały się Wam nasze usprawnienia jakie przygotowaliśmy dla Drupala. Jeżeli używacie jakiegoś rozwiązania dedykowanego dla Drupala, o którym nie wspomnieliśmy, koniecznie dajcie nam znać, też chętnie rozszerzymy naszą konfigurację o kolejne fajne moduły. Jeśli dopiero zaczynacie przygodę z Codeception to zachęcamy do samodzielnego eksperymentowania i pisania jak największej ilości testów, gdyż jak wiadomo “praktyka czyni mistrza”. Wszystkich natomiast zachęcamy do regularnego odwiedzania naszego blogu, gdzie na pewno jeszcze nie raz napiszemy coś o tym narzędziu.

3. Najlepsze praktyki zespołów programistycznych