Nasza strona używa cookies. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

10 rad dotyczących pisania testów funkcjonalnych w Symfony

Taras Tymoshchuk CEO / Product Manager / Geniusee
Sprawdź, jak pisać testy funkcjonalne w Symfony, aby polepszyć jakość Twojego kodu. Oto 10 konkretnych rad.
10 rad dotyczących pisania testów funkcjonalnych w Symfony

Po testowaniu ponad 50 projektów, widzę wyraźnie, jak bardzo testy mogą polepszyć jakość naszego kodu - zaczynając od oszczędzania czasu całego zespołu programistów po pomoc w zaspokajaniu potrzeb biznesowych.

Dla tych, którzy nie znają ich z doświadczenia, wyjaśnijmy najpierw, co oznacza termin „testy funkcjonalne” w Symfony. W dokumentacji znajduje się taka definicja:

„Testy funkcjonalne weryfikują integrację różnych poziomów aplikacji (od routingu po jej wygląd)”.

W istocie są to testy typu end-to-end. Piszesz kod, który wysyła żądania HTTP do aplikacji. Otrzymujesz odpowiedź HTTP i na jej dowiadujesz się jak i czy aplikacja działa. Można też dokonać zmian w bazie danych w poziomie przechowywania danych, co czasem daje dodatkowe możliwości sprawdzania statusu.

Przedstawię Wam teraz 10 protipów dotyczących pisania testów funkcjonalnych w Symfony, w oparciu o doświadczenie moje oraz mojego zespołu z Geniusee.


1. Testowanie z warstwą przechowywania danych

Prawdopodobnie pierwszą rzeczą, którą zechcesz zrobić podczas przeprowadzania testów funkcjonalnych, będzie oddzielenie bazy testowej od developerskiej. Ma to na celu stworzenie czystego środowiska do przeprowadzania testów i umożliwia doprowadzenie do żądanego stanu aplikacji i zarządzanie nim. Poza tym testy nie powinny zapisywać losowych danych w bazie developerskiej.

Zazwyczaj podczas uruchamiania testów aplikacja Symfony połączona jest z inną bazą danych. Jeśli masz Symfony 4 lub 5, możesz zdefiniować zmienne środowiska w pliku .env.test, które będą używane do testowania. Skonfiguruj również PHPUnit, aby zmienić zmienną środowiska APP_ENV do testowania. Na szczęście dzieje się tak domyślnie po zainstalowaniu komponentu Symfony w PHPUnit Bridge.

W przypadku wersji poniżej 4, możesz użyć rozruchu kernela w trybie testowym podczas przeprowadzania testów funkcjonalnych. Korzystając z plików config_test.yml, możesz zdefiniować konfigurację testu.


2. LiipFunctionalTestBundle

Ten pakiet zawiera kilka ważnych narzędzi wspomagających pisanie testów dla Symfony. Czasami próbuje wykonywać za dużo czynności i może nieraz wchodzić w drogę, ale ogólnie ułatwia pracę.

Podczas testowania np. można emulować dane wejściowe użytkownika, przesyłać dane fixture’ów, liczyć zapytania do bazy danych w celu sprawdzenia wydajności itp. Zalecam zainstalowanie tego pakietu na początku testowania nowej aplikacji Symfony.


3. Czyszczenie bazy danych po każdym teście

Kiedy należy czyścić bazę w trakcie testów? Narzędzia zapewnione przez Symfony nie wymuszają tu żadnego standardu. Preferujemy aktualizować bazę danych po zastosowaniu każdej metody testowej. Zestaw testów wygląda następująco:

<?php
namespace Tests;
use Tests\BaseTestCase;
class SomeControllerTest extends TestCase
{
public function test_a_registered_user_can_login()
{
// Clean slate. Database is empty.
// Create your world. Create users, roles and data.
// Execute logic
// Assert the outcome.
// Database is reset.
}

}


Świetnym sposobem na wyczyszczenie bazy danych jest załadowanie pustych fixture’ów do specjalnej metody setUp w PHPUnit. Możesz to zrobić, jeśli zainstalowałeś LiipFunctionalTestBundle.

<?php
namespace Tests;
class BaseTestCase extends PHPUnit_Test_Case
{
public function setUp()
{
$this->loadFixtures([]);
}
}


4. Kreowanie danych

Jeśli zaczynasz każdy test z pustą bazą danych, potrzebujesz kilku narzędzi do utworzenia danych testowych. Mogą to być dane w bazie danych lub encje.

Laravel podchodzi do tego w bardzo prosty sposób i używa fabryk testowy obiektów. Jest to fajne podejście, gdyż możemy stworzyć interfejsy tworzące obiekty, których często używamy w testach.. Oto przykład prostego interfejsu, który tworzy encje modelu User:

<?php
namespace Tests\Helpers;
use AppBundle\Entity\User;
trait CreatesUsers
{
public function makeUser(): User
{
$user = new User();
$user->setEmail($this->faker->email);
$user->setFirstName($this->faker->firstName);
$user->setLastName($this->faker->lastName);
$user->setRoles([User::ROLE_USER]);
$user->setBio($this->faker->paragraph);
return $user;
}


Możemy dodać takie interfejsy do wybranego zestawu testów:

<?php
namespace Tests;
use Tests\BaseTestCase;
use Tests\Helpers\CreatesUsers;
class SomeControllerTest extends TestCase
{
use CreatesUsers;
public function test_a_registered_user_can_login()
{
$user = $this->createUser();
// Login as a user. Do some tests.
}
}


5. Zastępowanie usług w kontenerze

W aplikacji Laravel bardzo łatwo jest zamienić usługi w kontenerze, ale w projektach Symfony jest to trudniejsze. W wersjach Symfony 3.4–4.1, usługi w kontenerze są oznaczone jako prywatne. To znaczy, że podczas pisania testów po prostu nie możesz podmienić jej na coś innego (np. stuba).

Niektórzy twierdzą, że testy funkcjonalne nie potrzebują stubów. Mogą jednak wystąpić sytuacje, w których nie masz dostępu do instancji usług dostarczanych z zewnątrz, do których można bezpieczenie wysyłać losowe, testowe dane.

Na szczęście w Symfony 4.1 możesz uzyskać dostęp do kontenera i zmieniać usługi zgodnie z zapotrzebowaniem, np.:

<?php
namespace Tests\AppBundle;
use AppBundle\Payment\PaymentProcessorClient;
use Tests\BaseTestCase;
class PaymentControllerTest extends BaseTestCase
{
public function test_a_user_can_purchase_product()
{
$paymentProcessorClient = $this->createMock(PaymentProcessorClient::class);
$paymentProcessorClient->expects($this->once())
->method('purchase')
->willReturn($successResponse);
// this is a hack to make the container use the mocked instance after the redirects
$client->disableReboot();
$client->getContainer()->set(PaymentProcessorClient::class, $paymentProcessorClient)
}
}


Należy jednak pamiętać, że podczas testów funkcjonalnych Symfony może się przeładowywać, od początku odczytując zależności, przez co może odrzucić Twoje stuby.


6. Wykonywanie SQLite w pamięci

SQLite jest często używany jako warstwa przechowywania danych podczas testowania, ponieważ jest bardzo kompaktowy i łatwy w konfiguracji. Te cechy sprawiają również, że jest bardzo wygodny w użyciu w środowiskach CI/CD.

SQLite nie wymaga specjalnego serwera, czyli odczytuje i zapisuje wszystkie dane z pliku. To może być wąskie gardło, bo dodaje dużo operacji I/O, których wykonanie będzie musiało poczekać na dostęp do zasobów. Dlatego możesz użyć opcji in-memory. Wtedy dane zostaną zapisane i odczytane z pamięci, co finalnie może przyspieszyć operacje.

Konfigurując bazę danych w aplikacji Symfony, nie musisz dodawać pliku database.sqlite - wystarczy tu użycie słowa kluczowego: memory:.


7. Wykonywanie SQLite w pamięci za pomocą tmpfs

Praca w pamięci jest świetna, ale czasami może być bardzo trudno skonfigurować ten tryb ze starą wersją LiipFunctionalTestBundle. Jeśli masz ten problem, mam dla Ciebie protip.

W systemach Linuxa można przydzielić część pamięci RAM-u, która będzie zachowywać się jak normalna pamięć masowa. Nazywa się to tmpfs. Musisz utworzyć folder tmpfs, umieszczasz w nim plik SQLite i używasz do uruchamiania testów.

Możesz użyć tego samego podejścia w przypadku MySQL, jednak konfiguracja będzie wtedy bardziej skomplikowana.


8. Testowanie z Elasticsearch

Podobnie jak w przypadku łączenia się z testową instancją bazy danych, można połączyć się również z testową instancją Elasticsearch. Lub jeszcze lepiej: w ten sposób możesz użyć też innych nazw indeksów, aby oddzielić środowiska testowe i programistyczne.

Testowanie Elasticsearch wydaje się proste, ale w praktyce może być różnie. Mamy potężne narzędzia do generowania schematów baz danych, tworzenia fixture’ów i wypełniania baz danych danymi testowymi. Te narzędzia mogą nie istnieć w przypadku Elasticsearch i trzeba wtedy stworzyć własne rozwiązania. Stąd może być trudno od tak zacząć testy.

Pojawia się również problem indeksowania nowych danych i zapewnienia dostępności informacji. Częstym błędem jest interwał aktualizacji Elasticsearch. Zazwyczaj indeksowane dokumenty stają się możliwe do przeszukiwania po czasie określonym w konfiguracji. Domyślnie ta 1 sekunda może stać się korkiem, który skutecznie przytka wykonanie testów..


9. Korzystanie z filtrów Xdebug w celu przyspieszenia raportowania pokrycia

Pokrycie jest ważnym aspektem testowania. Nie ma potrzeby traktowania tego jako najważniejszego elementu, ale ta cecha pozwala namierzyć nieprzetestowane gałęzie i wątki w kodzie. Zazwyczaj to Xdebug jest odpowiedzialny za ocenę pokrycia.

Przekonasz się, że przygotowanie analizy pokrycia znacznie spowalnia testy. Może to stanowić problem w środowisku CI/CD, gdzie każda minuta jest cenna.

Na szczęście można wprowadzić pewne optymalizacje. Kiedy Xdebug generuje raport pokrycia przeprowadzonych testów, robi to dla każdego pliku PHP w teście. Obejmuje to pliki znajdujące się w folderze vendora - czyli kodu, który nie należy do nas.

Konfigurując filtry pokrycia kodu w Xdebug, możemy sprawić, że nie będzie on generował raportów dotyczących plików, których nie potrzebujemy. W ten sposób oszczędzimy dużo czasu.

Jak tworzyć filtry? Można to zrobić za pomocą PHPUnit. Do utworzenia pliku konfiguracyjnego filtru potrzebne jest tylko jedno polecenie:

phpunit --dump-xdebug-filter build/xdebug-filter.php


Następnie przekazujemy tę konfigurację podczas uruchamiania testów:

phpunit --prepend build/xdebug-filter.php --coverage-html 
build/coverage-report


10. Narzędzia do zrównoleglania testów

Przeprowadzanie testów funkcjonalnych może zajmować dużo czasu. Przykładowo uruchomienie pełnego zestawu 77 testów i 524 założeń może zająć 3-4 minuty. Jest to normalne, biorąc pod uwagę, że każdy test wysyła kilka zapytań do bazy danych, generuje klastry wzorców, uruchamia roboty indeksujące te wzorce i przyjmuje na tej podstawie pewne założenia.

Jeśli otworzysz monitor aktywności, zauważysz, że testy wykorzystują tylko jeden rdzeń procesora. Możesz zaktywizować więcej rdzeni, używając narzędzi do paralelizacji, takich jak paratest lub fastest. Są łatwe w konfiguracji do uruchamiania testów jednostkowych. Jeśli jednak będziesz potrzebować połączenia z bazą danych, trzeba będzie się trochę pogłowić.

Mam nadzieję, że te wskazówki okażą się przydatne. Powodzenia!


Oryginał tekstu w języku angielskim przeczytasz tutaj.

Rozpocznij dyskusję

Lubisz dzielić się wiedzą i chcesz zostać autorem?

Podziel się wiedzą z 160 tysiącami naszych czytelników

Dowiedz się więcej