Pas 17: Testare
Testare¶
Din moment ce adăugăm din ce în ce mai multe funcționalități în aplicație, este probabil momentul potrivit pentru a vorbi despre testare.
Fapt amuzant: am găsit o eroare în timp ce scriam testele în acest capitol.
Symfony se bazează pe PHPUnit pentru teste unitare. Să-l instalăm:
1 | $ symfony composer req phpunit --dev
|
Elaborarea testelor unitare¶
SpamChecker
este prima clasă pentru care vom scrie teste. Generează un test unitar:
1 | $ symfony console make:unit-test SpamCheckerTest
|
Testarea SpamChecker este o provocare, deoarece cu siguranță nu vrem să atingem API-ul Akismet. Drept alternativă vom simula API-ul.
Să elaborăm un prim test pentru situația în care API-ul returnează o eroare:
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);
}
}
|
Clasa MockHttpClient
face posibilă simularea oricărui server HTTP. Este nevoie de o serie de instanțe MockResponse
care conțin anteturile preconizate ale corpului și răspunsului.
Apoi, apelăm la metoda getSpamScore()
și verificăm dacă o excepție este aruncată prin metoda expectException()
a PHPUnit.
Execută testele pentru a verifica dacă acestea trec:
1 | $ symfony php bin/phpunit
|
Să adăugăm teste pentru calea fericită:
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];
+ }
}
|
Furnizorii de date PHPUnit ne permit să reutilizăm aceeași logică de testare pentru mai multe cazuri de testare.
Redactarea testelor funcționale pentru controlere¶
Testarea controlerelor este puțin diferită de testarea unei clase PHP „obișnuite”, deoarece dorim să le executăm în contextul unei solicitări HTTP.
Instalează dependențele suplimentare necesare testelor funcționale:
1 | $ symfony composer req browser-kit --dev
|
Creează un test funcțional pentru controlerul Conference:
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');
}
}
|
Acest prim test verifică dacă pagina principală returnează un răspuns HTTP 200.
Variabila $client
simulează un browser. Totuși, în loc să efectueze apeluri HTTP către server, apelează direct aplicația Symfony. Această strategie are mai multe avantaje: este mult mai rapidă decât efectuarea transmiterea mesajelor dus-întors între client și server, dar permite și testele să introspecte starea serviciilor după fiecare solicitare HTTP.
Validări precum assertResponseIsSuccessful
sunt adăugate peste cele existente în PHPUnit pentru a-ți ușura munca. Există multe astfel de validări definite de Symfony.
Sfat
Am folosit /
pentru URL în loc să-l generăm prin router. Acest lucru se face în mod intenționat, deoarece testarea adreselor URL ale utilizatorului final face parte din ceea ce dorim să testăm. Dacă schimbi calea rutelor, testele vor da eroare, amintindu-ți că ar trebui să redirecționezi vechiul URL-ul vechi către unul nou, pentru a păstra funcționale legăturile din motoarele de căutare și de la alte site-urile web.
Notă
Am fi putut genera testul prin pachetul maker:
1 | $ symfony console make:functional-test Controller\\ConferenceController
|
Testele PHPUnit sunt executate într-un mediu test
dedicat. Trebuie să setăm secretul AKISMET_KEY
pentru acest mediu:
Execută noile teste doar transmițând calea către clasa lor:
1 | $ symfony php bin/phpunit tests/Controller/ConferenceControllerTest.php
|
Sfat
Când un test nu trece, poate fi utilă analizarea obiectului Response. Accesează-l prin $client->getResponse()
și echo
pentru a vedea cum arată.
Definirea datelor de test¶
Pentru a putea testa lista de comentarii, paginarea și trimiterea formularului, trebuie să populăm baza de date cu ceva informații. Și dorim ca datele să fie aceleași între teste pentru ca acestea să fie executate cu succes. Datele de testare sunt exact ceea ce avem nevoie.
Instalează pachetul Doctrine Fixtures:
1 | $ symfony composer req orm-fixtures --dev
|
În timpul instalării a fost creat un nou director src/DataFixtures/
, cu o clasă exemplu, gata de a fi personalizată. Adaugă acum două conferințe și un singur comentariu:
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\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();
}
|
Când vom încărca datele de testare, toate datele vor fi eliminate; inclusiv utilizatorul admin. Pentru a evita acest lucru, să adăugăm utilizatorul administrator în test:
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\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();
}
}
|
Sfat
Dacă nu îți amintești ce serviciu trebuie să utilizezi pentru o sarcină dată, utilizează comanda debug:autowiring
urmată de un cuvânt cheie:
1 | $ symfony console debug:autowiring encoder
|
Încărcarea datelor de testare¶
Încarcă datele de test în baza de date. Fii atent această acțiune va șterge toate datele stocate în prezent în baza de date (dacă dorești să eviți acest comportament, continuă să citești).
1 | $ symfony console doctrine:fixtures:load
|
Navigarea unui site web în teste funcționale¶
Așa cum am văzut, clientul HTTP utilizat în teste simulează un browser, astfel încât putem naviga prin intermediul site-ului ca și cum am folosi un browser headless.
Adaugă un test nou care face clic pe o pagină de conferință din pagina principală:
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")');
+ }
}
|
Să descriem ce se întâmplă în acest test într-un limbaj simplu:
- Ca și primul test, navigăm spre pagina principală;
- Metoda
request()
returnează o instanță`Crawler
care ajută la găsirea de elemente pe pagină (cum ar fi link-uri, formulare sau orice ce poate fi accesat cu selectoare CSS sau XPath); - Mulțumită unui selector CSS, afirmăm că avem două conferințe listate pe pagina principală;
- Facem clic apoi pe linkul „View” (deoarece nu se poate face clic pe mai multe linkuri simultan, Symfony alege automat primul pe care îl găsește);
- Verificăm titlul paginii, răspunsul și pagina
<h2>
pentru a fi siguri că suntem pe pagina corectă (am fi putut verifica și traseul corespunzător); - În cele din urmă, verificăm că există 1 comentariu pe pagină.
div: contains()
nu este un selector CSS valid, dar Symfony are câteva completări frumoase, împrumutate de la jQuery.
În loc să facem clic pe text (adică View
), am fi putut selecta și linkul prin intermediul unui selector CSS:
1 | $client->click($crawler->filter('h4 + p a')->link());
|
Verificați dacă noul test se execută cu succes:
1 | $ symfony php bin/phpunit tests/Controller/ConferenceControllerTest.php
|
Lucrul cu o bază de date de testare¶
În mod implicit, testele sunt rulate în mediul test
Symfony așa cum este definit în fișierul phpunit.xml.dist
:
1 2 3 4 5 | <phpunit>
<php>
<server name="APP_ENV" value="test" force="true" />
</php>
</phpunit>
|
Dacă dorești să utilizezi o bază de date diferită pentru testele tale, suprascrie variabila de mediu DATABASE_URL
în fișierul .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
|
Încarcă datele de testare pentru mediul/baza de date test
:
1 | $ APP_ENV=test symfony console doctrine:fixtures:load
|
În continuarea acestui pas, nu vom redefini variabila de mediu DATABASE_URL
. Utilizarea aceleiași baze de date ca mediul dev
pentru teste are câteva avantaje pe care le vom vedea în secțiunea următoare.
Trimiterea unui formular într-un test funcțional¶
Vrei să treci la nivelul următor? Încearcă să adaugi un comentariu nou cu o fotografie la o conferință, de la un test, simulând o expediere a formularului. Asta pare ambițios, nu-i așa? Uită-te la codul necesar: nu e mai complex decât ceea ce am elaborat deja:
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")');
+ }
}
|
Pentru a expedia un formular prin submitForm()
, găsește numele de intrare datorită instrumentului DevTools a browser-ului sau prin panoul Symfony Profiler Form. Observă reutilizarea inteligentă a imaginii „În construcție”!
Execută testele din nou pentru a verifica dacă validările trec cu succes:
1 | $ symfony php bin/phpunit tests/Controller/ConferenceControllerTest.php
|
Un avantaj al utilizării bazei de date „dev” pentru teste este că poți verifica rezultatul într-un browser:

Reîncărcarea datelor de testare¶
Dacă execuți testele a doua oară, acestea ar trebui să eșueze. Deoarece acum există mai multe comentarii în baza de date, validarea care verifică numărul de comentarii este invalidă. Trebuie să resetăm starea bazei de date între fiecare execuție, reîncărcând datele de test înainte de fiecare execuție:
1 2 | $ symfony console doctrine:fixtures:load
$ symfony php bin/phpunit tests/Controller/ConferenceControllerTest.php
|
Automatizarea fluxului de lucru cu un Makefile¶
Necesitatea de a-ți aminti o secvență de comenzi pentru a rula testele este incomodă. Acest lucru ar trebui cel puțin să fie documentat. Dar documentația ar trebui să fie o ultimă soluție. În schimb, cum rămâne cu automatizarea activităților de zi cu zi? Aceasta ar servi drept documentare, ar ajuta alți dezvoltatori și ar face viața dezvoltatorilor mai ușoară și rapidă.
Folosirea unui Makefile
este o modalitate de a automatiza comenzile:
1 2 3 4 5 6 | SHELL := /bin/bash
tests:
symfony console doctrine:fixtures:load -n
symfony php bin/phpunit
.PHONY: tests
|
Rețineți opțiunea -n
pentru comanda Doctrine; este o opțiune globală pentru comenzile Symfony care le face să nu fie interactive.
Ori de câte ori dorești să rulezi testele, folosește make tests
:
1 | $ make tests
|
Resetarea bazei de date după fiecare test¶
Resetarea bazei de date după fiecare testare este plăcută, dar executarea testelor independent este și mai bine. Nu dorim ca un test să se bazeze pe rezultatele celor anterioare. Modificarea ordinii testelor nu ar trebui să schimbe rezultatul. După cum o să ne dăm seama acum, nu este cazul deocamdată.
Mută testul testConferencePage
după testul testCommentSubmission
:
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
$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")');
+ }
}
|
Testele acum eșuează.
Pentru a reseta baza de date între teste, instalează DoctrineTestBundle:
1 | $ symfony composer req "dama/doctrine-test-bundle:^6" --dev
|
Va trebui să confirmi execuția rețetei (deoarece nu este un pachet „oficial” acceptat):
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
|
Activează ascultătorul PHPUnit:
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 gata. Orice modificări efectuate în teste sunt acum revizuite automat la sfârșitul fiecărui test.
Testele ar trebui să fie din nou validate:
1 | $ make tests
|
Utilizarea unui browser real pentru teste funcționale¶
Testele funcționale utilizează un browser special care apelează direct stratul Symfony. Dar, de asemenea, poți utiliza un browser real și stratul HTTP real datorită Symfony Panther:
1 | $ symfony composer req panther --dev
|
Poți scrie teste care utilizează un browser Google Chrome real cu următoarele modificări:
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_PROJECT_DEFAULT_ROUTE_URL']]);
$client->request('GET', '/');
$this->assertResponseIsSuccessful();
|
Variabila de mediu SYMFONY_PROJECT_DEFAULT_ROUTE_URL
conține adresa URL a serverului web local.
Executarea testelor funcționale în formatul cutiei negre cu Blackfire¶
Un alt mod de a rula teste funcționale este de a utiliza Blackfire player. Pe lângă ceea ce poți face cu teste funcționale, poate efectua și teste de performanță.
Consultă punctul „Performanță” pentru a afla mai multe.
- « Previous Pas 16: Prevenirea spamului cu un API
- Next » Pas 18: Procesând asincron
This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.