Schritt 17: Testen

5.0 version
Maintained

Testen

Da wir nun mehr und mehr Funktionalität in die Anwendung einbauen, ist es wahrscheinlich der richtige Zeitpunkt, über das Testen zu sprechen.

Lustige Tatsache: Ich habe beim Schreiben der Tests in diesem Kapitel einen Fehler gefunden.

Symfony setzt bei Unit-Tests auf PHPUnit. Lass es uns installieren:

1
$ symfony composer req phpunit

Unit-Tests schreiben

SpamChecker ist die erste Klasse, für die wir Tests schreiben werden. Generiere einen Unit-Test:

1
$ symfony console make:unit-test SpamCheckerTest

Das Testen des SpamCheckers ist eine Herausforderung, da wir die Akismet-API sicherlich nicht ständig aufrufen wollen. Wir werden die API mocken (simulieren).

Lasse uns einen ersten Test für den Fall schreiben, dass die API einen Fehler zurückgibt:

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);
     }
 }

Die MockHttpClient-Klasse ermöglicht es, jeden beliebigen HTTP-Server zu simulieren. Es wird eine Reihe von MockResponse-Instanzen benötigen, die den erwarteten Body und die Response-Header enthalten.

Anschließend rufen wir die getSpamScore()-Methode auf und überprüfen, mit Hilfe der expectException()-Methode von PHPUnit, ob eine Ausnahme ausgelöst wird.

Führe die Tests aus, um sicherzustellen, dass sie erfolgreich sind:

1
$ symfony php bin/phpunit

Lasst uns Tests für den happy path hinzufügen:

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];
+    }
 }

Der PHPUnit Data Provider ermöglicht es uns, die gleiche Testlogik für mehrere Testfälle wiederzuverwenden.

Funktionale Tests für Controller schreiben

Das Testen von Controllern ist etwas anders als das Testen einer „normalen“ PHP-Klasse, da wir sie im Rahmen einer HTTP-Anfrage ausführen wollen.

Installiere einige zusätzliche Dependencies, die für Funktionale Tests erforderlich sind:

1
$ symfony composer require browser-kit --dev

Erstelle einen funktionalen Test für den Conference-Controller:

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');
    }
}

Dieser erste Test prüft, ob die Homepage eine HTTP-Response mit Status 200 zurückgibt.

Die $client-Variable simuliert einen Browser. Anstatt jedoch HTTP-Anfragen an den Server zu senden, ruft dieser die Symfony-Anwendung direkt auf. Dieses Vorgehen hat mehrere Vorteile: Es ist viel schneller als eine tatsächliche Kommunikation zwischen Client und Server, und sie ermöglicht es auch, in den Tests den Zustand der Services nach jedem HTTP-Request zu überprüfen.

Assertions wie assertResponseIsSuccessful werden zusätzlich zu PHPUnit hinzugefügt, um Dir die Arbeit zu erleichtern. Symfony stellt viele solcher Assertions zur Verfügung.

Tipp

Wir haben / fix als URL verwendet, anstatt sie über den Router zu generieren. Dies geschieht absichtlich, da das Testen von Produktiv-URLs Teil dessen ist, was wir testen wollen. Sobald Du den Routenpfad änderst, werden die Tests fehlschlagen und dich dadurch freundlich daran erinnern, dass Du die alte URL wahrscheinlich auf die neue umleiten solltest, um gegenüber Suchmaschinen und Websites, die auf Deine Website verweisen, nett zu sein.

Bemerkung

Wir hätten den Test über das Maker-Bundle generieren können:

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

PHPUnit-Tests werden in einer dedizierten test-Environment ausgeführt. Wir müssen das AKISMET_KEY-Secret für diese Environment festlegen:

1
$ APP_ENV=test symfony console secrets:set AKISMET_KEY

Führe ausschließlich die neuen Tests aus, indem Du den Pfad zu ihrer Klasse übergibst:

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

Tipp

Wenn ein Test fehlschlägt, kann es sinnvoll sein, sich das Response-Objekt anzusehen. Greife über $client->getResponse() und echo darauf zu, um zu sehen, wie es aussieht.

Fixtures erstellen

Um die Kommentarliste, Pagination und die Formularübermittlung testen zu können, müssen wir die Datenbank mit Daten befüllen. Außerdem wollen wir, dass die Daten bei allen Testläufen identisch sind, damit die Tests erfolgreich durchlaufen. Fixtures sind genau das, was wir brauchen.

Installiere das Doctrine Fixtures Bundle:

1
$ symfony composer req orm-fixtures --dev

Während der Installation wurde ein neues src/DataFixtures/-Verzeichnis mit einer Beispielklasse erstellt, die angepasst werden kann. Füge vorerst zwei Konferenzen und einen Kommentar hinzu:

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();
     }

Wenn wir die Fixtures laden, werden alle Daten entfernt, einschließlich der Admin-User. Um das zu vermeiden, fügen wir den Admin-User den Fixtures hinzu:

 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();
     }
 }

Tipp

Falls Du Dich nicht mehr daran erinnerst, welchen Service Du für eine bestimmte Aufgabe verwenden musst, verwende debug:autowiring mit einem Keyword:

1
$ symfony console debug:autowiring encoder

Fixtures laden

Lade die Fixtures in die Datenbank. Achtung: Dabei werden alle derzeit in der Datenbank gespeicherten Daten gelöscht (wenn Du dieses Verhalten vermeiden möchtest, lies weiter).

1
$ symfony console doctrine:fixtures:load

Eine Website in Funktionalen Tests crawlen

Wie wir gesehen haben, simuliert der in den Tests verwendete HTTP-Client einen Browser, sodass wir durch die Website navigieren können, als würden wir einen Headless-Browser verwenden.

Füge einen neuen Test hinzu, der von der Homepage aus auf eine Konferenzseite klickt:

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")');
+    }
 }

Lasse uns in einfachen Worten beschreiben, was in diesem Test passiert:

  • Wie beim ersten Test gehen wir auf die Homepage;
  • Die request()-Methode gibt eine Crawler-Instanz zurück, die hilft, Elemente auf der Seite zu finden (wie Links, Formulare oder alles, was Du mit CSS-Selektoren oder XPath erreichen kannst);
  • Mit Hilfe eines CSS-Selektors prüfen wir, dass zwei Konferenzen auf der Homepage aufgelistet sind;
  • Dann klicken wir auf den Link „View“ (Symfony kann nicht mehr als einen Link gleichzeitig anklicken, darum wählt es automatisch den ersten, den es findet);
  • Wir testen den Seitentitel, die Response und die Seitenüberschrift <h2>, um sicher zu gehen, dass wir auf der richtigen Seite sind (wir hätten auch die zugehörige Route überprüfen können);
  • Schließlich prüfen wir, dass es einen Kommentar auf der Seite gibt. div:contains() ist zwar kein gültiger CSS-Selektor, Symfony hat sich jedoch einige nützliche Ergänzungen von jQuery abgeschaut.

Anstatt auf den Text zu klicken (z.B. View), hätten wir den Link auch über einen CSS-Selektor auswählen können:

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

Überprüfe, ob der neue Test grün ist:

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

Mit einer Testdatenbank arbeiten

Standardmäßig werden Tests in der test-Symfony-Environment ausgeführt, welche in der phpunit.xml.dist-Datei definiert ist:

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

Wenn Du für Deine Tests eine andere Datenbank verwenden möchtest, überschreibe die Environment-Variable DATABASE_URL in der .env.test-Datei:

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

Lade die Fixtures für die test-Environment/Datenbank:

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

Für den Rest dieses Schrittes werden wir die Environment-Variable DATABASE_URL nicht neu definieren. Die Verwendung der gleichen Datenbank wie die dev-Environment für Tests hat einige Vorteile, die wir im nächsten Abschnitt sehen werden.

Ein Formular in einem Funktionalen Test abschicken

Möchtest Du das nächste Level erreichen? Versuche, einen neuen Kommentar mit einem Foto auf einer Konferenz aus einem Test heraus hinzuzufügen, indem Du das Abschicken eines Formulares simulierst. Das scheint ehrgeizig zu sein, nicht wahr? Schaue Dir den benötigten Code an: nicht komplexer als das, was wir bereits geschrieben haben:

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")');
+    }
 }

Um ein Formular über submitForm() abzuschicken, kannst Du die Namen der Felder über die Browser-DevTools oder über den Formular-Tab des Symfony Profilers finden. Beachte die clevere Wiederverwendung des „under construction“-Bildes!

Führe die Tests erneut durch, um sicherzustellen, dass alles grün ist:

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

Ein Vorteil der Verwendung der „dev“-Datenbank für Tests ist, dass Du das Ergebnis in einem Browser überprüfen kannst:

Fixtures erneut laden

Wenn Du die Tests ein zweites Mal ausführst, sollten sie fehlschlagen. Da es nun mehr Kommentare in der Datenbank gibt, ist die Assertion, welche die Anzahl der Kommentare überprüft, nicht mehr korrekt. Wir müssen den Zustand der Datenbank zwischen jedem Durchlauf zurücksetzen, indem wir die Fixtures vor jedem Durchlauf neu laden:

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

Deinen Workflow mit einem Makefile automatisieren

Es ist ärgerlich, sich eine Reihe von Befehlen merken zu müssen, um die Tests auszuführen. Dies sollte zumindest dokumentiert werden. Eine Dokumentation sollte jedoch nur der letzte Ausweg sein. Wie sieht es stattdessen mit der Automatisierung der täglichen Aktivitäten aus? Das würde als Dokumentation dienen, anderen Entwickler*innen helfen, sie zu entdecken und ihre Arbeit erleichtern und beschleunigen.

Die Verwendung von einem Makefile ist eine Möglichkeit, Befehle zu automatisieren:

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

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

Beachte das -n-Flag des Doctrine Befehls; es ist ein globales Flag für Symfony Befehle, das sie nicht interaktiv macht.

Wann immer Du die Tests ausführen möchtest, verwende make tests:

1
$ make tests

Die Datenbank nach jedem Test zurücksetzen

Das Zurücksetzen der Datenbank nach jedem Testlauf ist schön, aber wirklich unabhängige Tests sind noch besser. Wir wollen nicht, dass sich ein Test auf die Ergebnisse der vorherigen stützt. Eine Änderung der Reihenfolge der Tests sollte das Ergebnis nicht verändern. Wie wir jetzt herausfinden werden, ist dies im Moment nicht der Fall.

Verschiebe den testConferencePage-Test hinter den testCommentSubmission-Test:

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")');
+    }
 }

Jetzt schlagen die Tests fehl.

Installiere das DoctrineTestBundle, um die Datenbank zwischen den Tests zurückzusetzen:

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

Du musst die Ausführung des Recipes bestätigen (da es sich nicht um ein „offiziell“ unterstütztes Bundle handelt):

 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

Aktiviere den PHPUnit-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>

Und fertig. Alle Änderungen, die in Tests vorgenommen werden, werden nun am Ende jedes Tests automatisch zurückgesetzt.

Die Tests sollten wieder grün sein:

1
$ make tests

Einen echten Browser für Funktionale Tests verwenden

Funktionale Tests verwenden einen speziellen Browser, der den Symfony-Layer direkt aufruft. Aber Du kannst auch einen echten Browser und den echten HTTP-Layer dank Symfony Panther verwenden:

Warnung

Als ich diesen Absatz schrieb, war es nicht möglich, Panther auf einem Symfony 5-Projekt zu installieren, da eine Abhängigkeit noch nicht kompatibel war.

1
$ symfony composer req panther --dev

Du kannst dann Tests schreiben, die einen echten Google Chrome-Browser verwenden. Dazu benötigst Du die folgenden Änderungen:

 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();

Die Environment-Variable SYMFONY_DEFAULT_ROUTE_URL enthält die URL des lokalen Webservers.

Funktionale „Black Box“-Tests mit Blackfire durchführen

Eine weitere Möglichkeit, Funktionale Tests durchzuführen, ist die Verwendung des Blackfire-Players. Zusätzlich zu dem, was Du mit Funktionalen Tests machen kannst, kann der Blackfire-Player auch Performance Tests durchführen.

Schau Dir den Schritt über „Performance“ an, um mehr zu erfahren.


  • « Previous Schritt 16: Spam mit Hilfe einer API verhindern
  • Next » Schritt 18: Asynchrone Verarbeitung

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