Etap 17: Testowanie

5.0 version
Maintained

Testowanie

Ponieważ zaczynamy dodawać coraz więcej i więcej funkcji do naszej aplikacji, jest to prawdopodobnie dobry czas, by poruszyć temat testowania.

Ciekawostka: Znalazłem błąd podczas pisania testów do tego rozdziału.

Symfony używa PHPUnit do testów jednostkowych. Zainstalujmy go:

1
$ symfony composer req phpunit

Pisanie testów jednostkowych

SpamChecker będzie pierwszą klasą, dla której napiszemy testy. Wygeneruj test jednostkowy:

1
$ symfony console make:unit-test SpamCheckerTest

Testowanie SpamCheckera jest niemałym wyzwaniem, ponieważ nie chcemy się łączyć z prawdziwym API Akismet. Będziemy musieli stworzyć atrapę (ang. mock).

Napiszmy nasz pierwszy test dla przypadku, kiedy API zwraca błąd w odpowiedzi:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
--- a/tests/SpamCheckerTest.php
+++ b/tests/SpamCheckerTest.php
@@ -2,12 +2,26 @@

 namespace App\Tests;

+use App\Entity\Comment;
+use App\SpamChecker;
 use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpClient\MockHttpClient;
+use Symfony\Component\HttpClient\Response\MockResponse;
+use Symfony\Contracts\HttpClient\ResponseInterface;

 class SpamCheckerTest extends TestCase
 {
-    public function testSomething()
+    public function testSpamScoreWithInvalidRequest()
     {
-        $this->assertTrue(true);
+        $comment = new Comment();
+        $comment->setCreatedAtValue();
+        $context = [];
+
+        $client = new MockHttpClient([new MockResponse('invalid', ['response_headers' => ['x-akismet-debug-help: Invalid key']])]);
+        $checker = new SpamChecker($client, 'abcde');
+
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionMessage('Unable to check for spam: invalid (Invalid key).');
+        $checker->getSpamScore($comment, $context);
     }
 }

Klasa MockHttpClient pozwala na stworzenie atrapy (ang. „mock”) dla dowolnego serwera HTTP. Jako argument przyjmuje ona tablicę instancji MockResponse z oczekiwaną odpowiedzią i nagłówkami.

Następnie wywołujemy getSpamScore() i przez metodę expectException() w PHPUnit sprawdzamy, czy otrzymaliśmy wyjątek.

Uruchom testy, by sprawdzić, czy wykonują się poprawnie:

1
$ symfony php bin/phpunit

Dodajmy testy dla przypadku, gdy wszystko przejdzie bezbłędnie (tzw. happy path):

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
--- a/tests/SpamCheckerTest.php
+++ b/tests/SpamCheckerTest.php
@@ -24,4 +24,32 @@ class SpamCheckerTest extends TestCase
         $this->expectExceptionMessage('Unable to check for spam: invalid (Invalid key).');
         $checker->getSpamScore($comment, $context);
     }
+
+    /**
+     * @dataProvider getComments
+     */
+    public function testSpamScore(int $expectedScore, ResponseInterface $response, Comment $comment, array $context)
+    {
+        $client = new MockHttpClient([$response]);
+        $checker = new SpamChecker($client, 'abcde');
+
+        $score = $checker->getSpamScore($comment, $context);
+        $this->assertSame($expectedScore, $score);
+    }
+
+    public function getComments(): iterable
+    {
+        $comment = new Comment();
+        $comment->setCreatedAtValue();
+        $context = [];
+
+        $response = new MockResponse('', ['response_headers' => ['x-akismet-pro-tip: discard']]);
+        yield 'blatant_spam' => [2, $response, $comment, $context];
+
+        $response = new MockResponse('true');
+        yield 'spam' => [1, $response, $comment, $context];
+
+        $response = new MockResponse('false');
+        yield 'ham' => [0, $response, $comment, $context];
+    }
 }

Dostawcy danych (ang. data providers) w PHPUnit pozwalają nam na użycie jednego schematu testu dla wielu przypadków.

Pisanie testów funkcjonalnych dla kontrolerów

Testowanie kontrolerów jest nieco inne niż testowanie „zwykłej” klasy PHP, ponieważ chcemy je uruchamiać w kontekście żądania HTTP.

Zainstaluj kilka dodatkowych zależności, wymaganych dla testów funkcjonalnych:

1
$ symfony composer require browser-kit --dev

Stwórz test funkcjonalny dla kontrolera Conference:

tests/Controller/ConferenceControllerTest.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ConferenceControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $client = static::createClient();
        $client->request('GET', '/');

        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h2', 'Give your feedback');
    }
}

Ten pierwszy test sprawdza, czy strona główna zwraca 200 jako kod statusu odpowiedzi HTTP.

Zmienna $client symuluje przeglądarkę. Jednak zamiast łączyć się z serwerem przez HTTP, wykonuje ona kod bezpośrednio wewnątrz aplikacji Symfony. Strategia ta ma kilka zalet: jest znacznie szybsza niż wymiana danych pomiędzy klientem a serwerem oraz pozwala testom na sprawdzenie stanu serwisów po każdym żądaniu HTTP.

Symfony rozszerza PHPUnit dodając asercje typu assertResponseIsSuccessful, by ułatwić nam pracę. Istnieje wiele takich asercji zdefiniowanych przez Symfony.

Wskazówka

Użyliśmy / jako URL zamiast generowania go przez router. Jest to celowy zabieg, ponieważ testowanie adresów URL użytkownika końcowego jest częścią tego, co chcemy przetestować. Jeśli w przyszłości zmieni się ścieżka, testy przypomną Ci, że prawdopodobnie powinno zostać dodane przekierowanie ze starego adresu na nowy, aby być przyjaznym dla wyszukiwarek i stron internetowych, które odsyłają do Twojej strony.

Informacja

Mogliśmy wygenerować ten test przez Maker Bundle:

1
$ symfony console make:functional-test Controller\\ConferenceController

PHPUnit uruchamia testy w dedykowanym środowisku testowym o nazwie test. Musimy ustalić klucz AKISMET_KEY dla tego środowiska:

1
$ APP_ENV=test symfony console secrets:set AKISMET_KEY

Uruchom nowe testy przekazując jedynie ścieżkę do ich klas:

1
$ symfony php bin/phpunit tests/Controller/ConferenceControllerTest.php

Wskazówka

Kiedy test kończy się niepowodzeniem, przydatny może okazać się wgląd do obiektu Response. Możesz się do niego dostać przez $client->getResponse() i użyć echo by sprawdzić jak on wygląda.

Definiowanie danych testowych (ang. fixtures)

Aby móc przetestować listę komentarzy, stronicowanie i wysyłanie formularza, musimy wypełnić bazę danych jakimiś danymi. Dodatkowo chcemy, aby te dane były niezmienne pomiędzy poszczególnymi testami. Dane testowe (ang. fixtures) są dokładnie tym, czego potrzebujemy.

Zainstaluj Doctrine Fixtures Bundle:

1
$ symfony composer req orm-fixtures --dev

Podczas instalacji został utworzony nowy folder src/DataFixtures/ z przykładową klasą, gotową do zmodyfikowania. Dodajmy dwie konferencje i jeden komentarz:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
--- a/src/DataFixtures/AppFixtures.php
+++ b/src/DataFixtures/AppFixtures.php
@@ -2,6 +2,8 @@

 namespace App\DataFixtures;

+use App\Entity\Comment;
+use App\Entity\Conference;
 use Doctrine\Bundle\FixturesBundle\Fixture;
 use Doctrine\Common\Persistence\ObjectManager;

@@ -9,8 +11,24 @@ class AppFixtures extends Fixture
 {
     public function load(ObjectManager $manager)
     {
-        // $product = new Product();
-        // $manager->persist($product);
+        $amsterdam = new Conference();
+        $amsterdam->setCity('Amsterdam');
+        $amsterdam->setYear('2019');
+        $amsterdam->setIsInternational(true);
+        $manager->persist($amsterdam);
+
+        $paris = new Conference();
+        $paris->setCity('Paris');
+        $paris->setYear('2020');
+        $paris->setIsInternational(false);
+        $manager->persist($paris);
+
+        $comment1 = new Comment();
+        $comment1->setConference($amsterdam);
+        $comment1->setAuthor('Fabien');
+        $comment1->setEmail('[email protected]');
+        $comment1->setText('This was a great conference.');
+        $manager->persist($comment1);

         $manager->flush();
     }

W trakcie ładowania danych testowych wszystkie dotychczasowe dane są usuwane. Aby uniknąć usunięcia konta administracyjnego, musimy dodać je do naszych danych testowych (ang. fixtures):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
--- a/src/DataFixtures/AppFixtures.php
+++ b/src/DataFixtures/AppFixtures.php
@@ -2,13 +2,22 @@

 namespace App\DataFixtures;

+use App\Entity\Admin;
 use App\Entity\Comment;
 use App\Entity\Conference;
 use Doctrine\Bundle\FixturesBundle\Fixture;
 use Doctrine\Common\Persistence\ObjectManager;
+use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;

 class AppFixtures extends Fixture
 {
+    private $encoderFactory;
+
+    public function __construct(EncoderFactoryInterface $encoderFactory)
+    {
+        $this->encoderFactory = $encoderFactory;
+    }
+
     public function load(ObjectManager $manager)
     {
         $amsterdam = new Conference();
@@ -30,6 +39,12 @@ class AppFixtures extends Fixture
         $comment1->setText('This was a great conference.');
         $manager->persist($comment1);

+        $admin = new Admin();
+        $admin->setRoles(['ROLE_ADMIN']);
+        $admin->setUsername('admin');
+        $admin->setPassword($this->encoderFactory->getEncoder(Admin::class)->encodePassword('admin', null));
+        $manager->persist($admin);
+
         $manager->flush();
     }
 }

Wskazówka

Jeśli nie pamiętasz, którego serwisu musisz użyć do danego zadania, użyj debug:autowiring ze słowami kluczowymi:

1
$ symfony console debug:autowiring encoder

Ładowanie danych testowych (ang. fixtures)

Załaduj dane testowe do bazy danych. Uwaga! Ta operacja usunie wszystkie obecne dane przechowywane w bazie danych (jeśli chcesz tego uniknąć, czytaj dalej).

1
$ symfony console doctrine:fixtures:load

Przeszukiwanie (ang. crawling) strony w testach funkcjonalnych

Jak widzieliśmy, klient HTTP użyty w testach symuluje przeglądarkę, dzięki czemu możemy poruszać się po stronie tak, jakbyśmy korzystali z przeglądarki bez interfejsu.

Dodaj nowy test, który kliknie w odnośnik do strony konferencji na stronie głównej:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
--- a/tests/Controller/ConferenceControllerTest.php
+++ b/tests/Controller/ConferenceControllerTest.php
@@ -14,4 +14,19 @@ class ConferenceControllerTest extends WebTestCase
         $this->assertResponseIsSuccessful();
         $this->assertSelectorTextContains('h2', 'Give your feedback');
     }
+
+    public function testConferencePage()
+    {
+        $client = static::createClient();
+        $crawler = $client->request('GET', '/');
+
+        $this->assertCount(2, $crawler->filter('h4'));
+
+        $client->clickLink('View');
+
+        $this->assertPageTitleContains('Amsterdam');
+        $this->assertResponseIsSuccessful();
+        $this->assertSelectorTextContains('h2', 'Amsterdam 2019');
+        $this->assertSelectorExists('div:contains("There are 1 comments")');
+    }
 }

Opiszmy prostymi słowami, co się dzieje w tym teście:

  • Tak jak w pierwszym teście, otwieramy stronę główną;
  • Metoda request() zwraca instancję klasy Crawler, która pomaga znaleźć elementy na stronie (takie jak odnośniki, formularze lub cokolwiek, do czego można dotrzeć za pomocą selektora CSS lub XPath);
  • Dzięki selektorowi CSS sprawdzamy, czy na stronie głównej są wyświetlone dwie konferencje;
  • Następnie klikamy w odnośnik „View” (jako że nie można kliknąć w więcej niż jeden odnośnik na raz, Symfony automatycznie wybierze pierwszy, który znajdzie);
  • Sprawdzamy tytuł strony, odpowiedź serwera i <h2> strony, by upewnić się, że znajdujemy się na właściwej (moglibyśmy również sprawdzić, czy ścieżka się zgadza);
  • I w końcu sprawdzamy, czy na stronie znajduje się jeden komentarz. div:contains() nie jest poprawnym selektorem CSS, jednak Symfony posiada parę takich dodatków zapożyczonych z jQuery.

Zamiast klikać w tekst (np. View), mogliśmy również wybrać link za pomocą selektora CSS:

1
$client->click($crawler->filter('h4 + p a')->link());

Sprawdź czy test przechodzi „na zielono”:

1
$ symfony php bin/phpunit tests/Controller/ConferenceControllerTest.php

Praca z testową bazą danych

Domyślnie testy aplikacji opartej o Symfony są uruchamiane w środowisku test zdefiniowanym w pliku phpunit.xml.dist:

phpunit.xml.dist
1
2
3
4
5
<phpunit>
    <php>
        <server name="APP_ENV" value="test" force="true" />
    </php>
</phpunit>

Jeśli chcesz użyć innej bazy danych dla testów, nadpisz zmienną środowiskową DATABASE_URL w pliku .env.test:

1
2
3
4
5
6
7
8
--- a/.env.test
+++ b/.env.test
@@ -1,4 +1,5 @@
 # define your env variables for the test env here
+DATABASE_URL=postgres://main:[email protected]:32773/test?sslmode=disable&charset=utf8
 KERNEL_CLASS='App\Kernel'
 APP_SECRET='$ecretf0rt3st'
 SYMFONY_DEPRECATIONS_HELPER=999999

Załaduj dane testowe dla środowiska/bazy danych test:

1
$ APP_ENV=test symfony console doctrine:fixtures:load

Przez resztę tego kroku, nie będziemy redefiniować zmiennej środowiskowej DATABASE_URL. Używanie dla testów tej samej bazy danych, co środowisko dev, ma pewne zalety, które zobaczymy w następnej sekcji.

Wysyłanie formularza przez test funkcjonalny

Chcesz wejść na wyższy poziom? Spróbuj dodać nowy komentarz ze zdjęciem przez formularz konferencji, symulując wysłanie formularza. Ambitne, prawda? Spójrz na potrzebny kod: nie jest bardziej skomplikowany niż ten, który już napisaliśmy:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
--- a/tests/Controller/ConferenceControllerTest.php
+++ b/tests/Controller/ConferenceControllerTest.php
@@ -29,4 +29,19 @@ class ConferenceControllerTest extends WebTestCase
         $this->assertSelectorTextContains('h2', 'Amsterdam 2019');
         $this->assertSelectorExists('div:contains("There are 1 comments")');
     }
+
+    public function testCommentSubmission()
+    {
+        $client = static::createClient();
+        $client->request('GET', '/conference/amsterdam-2019');
+        $client->submitForm('Submit', [
+            'comment_form[author]' => 'Fabien',
+            'comment_form[text]' => 'Some feedback from an automated functional test',
+            'comment_form[email]' => '[email protected]',
+            'comment_form[photo]' => dirname(__DIR__, 2).'/public/images/under-construction.gif',
+        ]);
+        $this->assertResponseRedirects();
+        $client->followRedirect();
+        $this->assertSelectorExists('div:contains("There are 2 comments")');
+    }
 }

Aby wysłać formularz za pośrednictwem submitForm(), znajdź nazwy elementów formularza, używając narzędzi deweloperskich w przeglądarce lub zakładki Form w panelu Symfony Profiler. Zwróć uwagę na przemyślane ponowne wykorzystanie obrazka „w budowie”!

Uruchom testy jeszcze raz, aby upewnić się, że wszystkie przechodzą „na zielono”:

1
$ symfony php bin/phpunit tests/Controller/ConferenceControllerTest.php

Jedną z zalet używania bazy danych „dev” dla testów jest to, że można sprawdzić rezultat w przeglądarce:

Ponowne ładowanie danych testowych (ang. fixtures)

Jeśli uruchomisz testy drugi raz, powinny one zakończyć się niepowodzeniem. Ponieważ w bazie danych znajduje się teraz więcej komentarzy, asercja sprawdzająca liczbę komentarzy nie będzie działać poprawnie. Musimy zresetować stan bazy danych pomiędzy każdym uruchomieniem poprzez załadowanie danych testowych (ang. fixtures):

1
2
$ symfony console doctrine:fixtures:load
$ symfony php bin/phpunit tests/Controller/ConferenceControllerTest.php

Automatyzacja pracy (ang. workflow) z pomocą pliku Makefile

Zapamiętywanie sekwencji poleceń do przeprowadzenia testów jest irytujące. Jednym z rozwiązań może być spisanie ich, jednak dokumentacja powinna być ostatecznością. Może zamiast tego powinniśmy zautomatyzować tę codzienną czynność? Byłaby to forma dokumentacji oraz ułatwienie i przyspieszenie pracy dla innych.

Używanie Makefile jest jednym ze sposobów zautomatyzowania poleceń:

Makefile
1
2
3
4
5
6
SHELL := /bin/bash

tests:
    symfony console doctrine:fixtures:load -n
    symfony php bin/phpunit
.PHONY: tests

Zwróć uwagę na flagę -n przy poleceniu Doctrine; jest to globalna flaga dla poleceń Symfony, która sprawia, że nie są one interaktywne.

Kiedykolwiek będziesz chciał uruchomić testy, użyj make tests:

1
$ make tests

Resetowanie bazy danych po każdym teście

Resetowanie bazy danych po każdym teście jest w porządku, ale używanie prawdziwie niezależnych testów jest jeszcze lepsze. Nie chcemy przecież, żeby jakikolwiek test opierał się na poprzednich wynikach. Zmiana kolejności testów nie powinna mieć wpływu na rezultat. Jak się zaraz przekonamy, nie jest to póki co prawdą.

Przenieś test testConferencePage tak, by znajdował się za testem testCommentSubmission:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
--- a/tests/Controller/ConferenceControllerTest.php
+++ b/tests/Controller/ConferenceControllerTest.php
@@ -15,21 +15,6 @@ class ConferenceControllerTest extends WebTestCase
         $this->assertSelectorTextContains('h2', 'Give your feedback');
     }

-    public function testConferencePage()
-    {
-        $client = static::createClient();
-        $crawler = $client->request('GET', '/');
-
-        $this->assertCount(2, $crawler->filter('h4'));
-
-        $client->clickLink('View');
-
-        $this->assertPageTitleContains('Amsterdam');
-        $this->assertResponseIsSuccessful();
-        $this->assertSelectorTextContains('h2', 'Amsterdam 2019');
-        $this->assertSelectorExists('div:contains("There are 1 comments")');
-    }
-
     public function testCommentSubmission()
     {
         $client = static::createClient();
@@ -44,4 +29,19 @@ class ConferenceControllerTest extends WebTestCase
         $crawler = $client->followRedirect();
         $this->assertSelectorExists('div:contains("There are 2 comments")');
     }
+
+    public function testConferencePage()
+    {
+        $client = static::createClient();
+        $crawler = $client->request('GET', '/');
+
+        $this->assertCount(2, $crawler->filter('h4'));
+
+        $client->clickLink('View');
+
+        $this->assertPageTitleContains('Amsterdam');
+        $this->assertResponseIsSuccessful();
+        $this->assertSelectorTextContains('h2', 'Amsterdam 2019');
+        $this->assertSelectorExists('div:contains("There are 1 comments")');
+    }
 }

Teraz testy zwracają błąd.

Aby resetować bazę danych pomiędzy testami, zainstaluj Doctrine Test Bundle:

1
$ symfony composer require dama/doctrine-test-bundle --dev

Będziesz musiał potwierdzić wykonanie przepisu (ang. recipe), ponieważ nie jest to „oficjalnie” obsługiwany pakiet (ang. bundle):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Symfony operations: 1 recipe (d7f110145ba9f62430d1ad64d57ab069)
  -  WARNING  dama/doctrine-test-bundle (>=4.0): From github.com/symfony/recipes-contrib:master
    The recipe for this package comes from the "contrib" repository, which is open to community contributions.
    Review the recipe at https://github.com/symfony/recipes-contrib/tree/master/dama/doctrine-test-bundle/4.0

    Do you want to execute this recipe?
    [y] Yes
    [n] No
    [a] Yes for all packages, only for the current installation session
    [p] Yes permanently, never ask again for this project
    (defaults to n): p

Dodaj nasłuchiwacz PHPUnit (ang. listener):

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -27,6 +27,10 @@
         </whitelist>
     </filter>

+    <extensions>
+        <extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" />
+    </extensions>
+
     <listeners>
         <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
     </listeners>

I gotowe. Wszelkie zmiany dokonywane przez testy będą teraz automatycznie cofane po zakończeniu każdego z nich.

Testy znowu powinny świecić się na zielono:

1
$ make tests

Korzystanie z prawdziwej przeglądarki do testów funkcjonalnych

Testy funkcjonalne wykorzystują specjalną przeglądarkę, która bezpośrednio wywołuje warstwę Symfony. Jednak dzięki Symfony Panther, możesz również użyć prawdziwej przeglądarki i prawdziwej warstwy HTTP:

Ostrzeżenie

W czasie gdy pisałem ten akapit, nie było jeszcze możliwe zainstalowanie Panther w Symfony 5, ponieważ jedna zależność nie była kompatybilna.

1
$ symfony composer req panther --dev

Następnie możesz pisać testy z użyciem prawdziwego Google Chrome z następującymi zmianami:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
--- a/tests/Controller/ConferenceControllerTest.php
+++ b/tests/Controller/ConferenceControllerTest.php
@@ -2,13 +2,13 @@

 namespace App\Tests\Controller;

-use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+use Symfony\Component\Panther\PantherTestCase;

-class ConferenceControllerTest extends WebTestCase
+class ConferenceControllerTest extends PantherTestCase
 {
     public function testIndex()
     {
-        $client = static::createClient();
+        $client = static::createPantherClient(['external_base_uri' => $_SERVER['SYMFONY_DEFAULT_ROUTE_URL']]);
         $client->request('GET', '/');

         $this->assertResponseIsSuccessful();

Zmienna środowiskowa SYMFONY_DEFAULT_ROUTE_URL zawiera adres URL lokalnego serwera WWW.

Uruchamianie czarnoskrzynkowych testów funkcjonalnych (ang. black box) przy użyciu Blackfire

Innym sposobem na przeprowadzenie testów funkcjonalnych jest użycie Blackfire player. Oprócz zwykłych testów funkcjonalnych, potrafi on również przeprowadzać testy wydajnościowe.

Aby dowiedzieć się więcej, zapoznaj się z krokiem „Wydajność”.


  • « Previous Etap 16: Ochrona przed spamem przy pomocy API
  • Next » Etap 18: Idziemy w asynchroniczność

This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.