Das Testen ist ein wesentlicher Bestandteil der Softwareentwicklung, um die Qualität und Stabilität einer Anwendung sicherzustellen. Symfony bietet umfangreiche Werkzeuge und Best Practices für das Testen von Anwendungen auf verschiedenen Ebenen, von Unit-Tests bis hin zu funktionalen Tests.
In diesem Artikel werden wir die folgenden Themen ausführlich behandeln:
- Einrichtung von PHPUnit für Symfony
- Schreiben von Unit-Tests für Services
- Funktionale Tests von Controllern und Routen
- Testen von Formularen und Validierung
- Verwendung von Test-Doubles und Mocks
1. Einrichtung von PHPUnit für Symfony
1.1 Was ist PHPUnit?
PHPUnit ist das de-facto Standard-Framework für Unit-Tests in PHP. Es ermöglicht das Schreiben und Ausführen von Tests, um die Funktionalität einzelner Komponenten sicherzustellen.
1.2 Installation von PHPUnit in Symfony
Symfony-Projekte werden oft mit PHPUnit vorinstalliert ausgeliefert. Falls nicht, können Sie PHPUnit mit Composer installieren.
Schritt 1: Installation von PHPUnit
Fügen Sie PHPUnit zu Ihren Entwicklungsabhängigkeiten hinzu:
composer require --dev phpunit/phpunit
Schritt 2: Installieren des Symfony Test Packs
Das Symfony Test Pack enthält nützliche Pakete für das Testen.
composer require --dev symfony/test-pack
Dies installiert unter anderem:
- symfony/phpunit-bridge: Brücke zwischen Symfony und PHPUnit.
- symfony/browser-kit: Simuliert einen Browser für funktionale Tests.
- symfony/css-selector: Erlaubt die Verwendung von CSS-Selektoren in Tests.
Schritt 3: Konfiguration von PHPUnit
Eine Standardkonfiguration befindet sich in der Datei phpunit.xml.dist
im Stammverzeichnis Ihres Projekts. Falls nicht vorhanden, können Sie diese erstellen.
Beispiel phpunit.xml.dist
:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="config/bootstrap.php">
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<php>
<ini name="error_reporting" value="-1"/>
<server name="KERNEL_CLASS" value="App\Kernel"/>
</php>
</phpunit>
Schritt 4: Initialisierung des Testverzeichnisses
Erstellen Sie das Verzeichnis tests/
im Stammverzeichnis Ihres Projekts, falls es nicht existiert.
2. Schreiben von Unit-Tests für Services
2.1 Was sind Unit-Tests?
Unit-Tests prüfen einzelne Einheiten des Codes, wie Klassen oder Methoden, isoliert von anderen Teilen des Systems.
2.2 Beispiel-Service zum Testen
Angenommen, wir haben einen einfachen Service, der mathematische Operationen ausführt.
// src/Service/Calculator.php
namespace App\Service;
class Calculator
{
public function add(int $a, int $b): int
{
return $a + $b;
}
public function divide(int $a, int $b): float
{
if ($b === 0) {
throw new \InvalidArgumentException('Division durch Null ist nicht erlaubt.');
}
return $a / $b;
}
}
2.3 Schreiben eines Unit-Tests
Schritt 1: Erstellen der Testklasse
Erstellen Sie eine Testklasse im Verzeichnis tests/Service/
.
// tests/Service/CalculatorTest.php
namespace App\Tests\Service;
use App\Service\Calculator;
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
public function testAdd()
{
$calculator = new Calculator();
$result = $calculator->add(5, 3);
$this->assertEquals(8, $result);
}
public function testDivide()
{
$calculator = new Calculator();
$result = $calculator->divide(10, 2);
$this->assertEquals(5, $result);
}
public function testDivideByZero()
{
$calculator = new Calculator();
$this->expectException(\InvalidArgumentException::class);
$calculator->divide(10, 0);
}
}
Schritt 2: Ausführen der Tests
Führen Sie die Tests mit dem folgenden Befehl aus:
php bin/phpunit
Erwartete Ausgabe:
PHPUnit 9.6.21 by Sebastian Bergmann and contributors.
Testing
... 3 / 3 (100%)
Time: 00:00.012, Memory: 8.00 MB
OK (3 tests, 3 assertions)
2.4 Assertions in PHPUnit
PHPUnit bietet viele Assertions, um Testbedingungen zu prüfen:
- assertEquals($expected, $actual): Prüft, ob zwei Werte gleich sind.
- assertTrue($condition): Prüft, ob eine Bedingung
true
ist. - assertFalse($condition): Prüft, ob eine Bedingung
false
ist. - assertNull($value): Prüft, ob ein Wert
null
ist. - assertCount($expectedCount, $array): Prüft die Anzahl der Elemente in einem Array.
3. Funktionale Tests von Controllern und Routen
3.1 Was sind funktionale Tests?
Funktionale Tests prüfen das Zusammenspiel mehrerer Komponenten, z. B. die Reaktion eines Controllers auf eine HTTP-Anfrage.
3.2 Beispiel-Controller zum Testen
// src/Controller/CalculatorController.php
namespace App\Controller;
use App\Service\Calculator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class CalculatorController extends AbstractController
{
#[Route('/add/{a}/{b}', name: 'add')]
public function add(int $a, int $b, Calculator $calculator): Response
{
$result = $calculator->add($a, $b);
return new Response("Ergebnis: $result");
}
}
3.3 Schreiben eines funktionalen Tests
Schritt 1: Erstellen der Testklasse
Erstellen Sie eine Testklasse im Verzeichnis tests/Controller/
.
// tests/Controller/CalculatorControllerTest.php
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class CalculatorControllerTest extends WebTestCase
{
public function testAdd()
{
$client = static::createClient();
$client->request('GET', '/add/5/3');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('body', 'Ergebnis: 8');
}
}
Schritt 2: Ausführen des Tests
php bin/phpunit
3.4 Verwendung des Test-Clients
Der Test-Client simuliert einen Browser und ermöglicht das Senden von HTTP-Anfragen.
- createClient(): Erstellt einen neuen Client.
- request($method, $uri, $parameters = [], $files = [], $server = [], $content = null): Sendet eine HTTP-Anfrage.
- getResponse(): Ruft die Antwort ab.
- getCrawler(): Ruft den Crawler ab, um das DOM zu durchsuchen.
3.5 Wichtige Assertions für funktionale Tests
- assertResponseIsSuccessful(): Prüft, ob der HTTP-Statuscode 2xx ist.
- assertResponseStatusCodeSame($statusCode): Prüft auf einen bestimmten Statuscode.
- assertSelectorTextContains($selector, $text): Prüft, ob ein bestimmter Text in einem HTML-Element enthalten ist.
- assertPageTitleContains($text): Prüft den Seitentitel.
4. Testen von Formularen und Validierung
4.1 Beispiel: Registrierungsformular
Angenommen, wir haben ein Registrierungsformular für Benutzer.
Formular-Typ
// src/Form/RegistrationFormType.php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class)
->add('plainPassword', PasswordType::class);
}
}
4.2 Schreiben eines Tests für das Formular
Schritt 1: Erstellen der Testklasse
// tests/Form/RegistrationFormTypeTest.php
namespace App\Tests\Form;
use App\Form\RegistrationFormType;
use App\Entity\User;
use Symfony\Component\Form\Test\TypeTestCase;
class RegistrationFormTypeTest extends TypeTestCase
{
public function testSubmitValidData()
{
$formData = [
'email' => 'user@example.com',
'plainPassword' => 'password123',
];
$model = new User();
$form = $this->factory->create(RegistrationFormType::class, $model);
$expected = new User();
$expected->setEmail('user@example.com');
$expected->setPlainPassword('password123');
// Daten dem Formular übermitteln
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
// Modell mit erwarteten Daten vergleichen
$this->assertEquals($expected->getEmail(), $model->getEmail());
$this->assertEquals($expected->getPlainPassword(), $model->getPlainPassword());
// Form View testen
$view = $form->createView();
$children = $view->children;
foreach (array_keys($formData) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
}
4.3 Validierungsregeln testen
Anpassung der Entität mit Validierung
// src/Entity/User.php
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class User
{
// ...
#[Assert\NotBlank]
#[Assert\Email]
private ?string $email = null;
#[Assert\NotBlank]
#[Assert\Length(min: 6)]
private ?string $plainPassword = null;
// Getter und Setter ...
}
Testen der Validierung
// tests/Entity/UserTest.php
namespace App\Tests\Entity;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class UserTest extends KernelTestCase
{
public function testValidUser()
{
self::bootKernel();
$container = static::getContainer();
$validator = $container->get('validator');
$user = new User();
$user->setEmail('user@example.com');
$user->setPlainPassword('password123');
$errors = $validator->validate($user);
$this->assertCount(0, $errors);
}
public function testInvalidEmail()
{
self::bootKernel();
$container = static::getContainer();
$validator = $container->get('validator');
$user = new User();
$user->setEmail('invalid-email');
$user->setPlainPassword('password123');
$errors = $validator->validate($user);
$this->assertCount(1, $errors);
}
}
5. Verwendung von Test-Doubles und Mocks
5.1 Was sind Test-Doubles und Mocks?
- Test-Doubles: Allgemeiner Begriff für Objekte, die reale Abhängigkeiten in Tests ersetzen.
- Mocks: Spezielle Art von Test-Doubles, die das Verhalten einer Abhängigkeit simulieren und Erwartungen setzen.
5.2 Verwendung von Mocks mit PHPUnit
Beispiel: Mocking eines Services
Angenommen, unser CalculatorController
verwendet einen externen Service, den wir mocken möchten.
// src/Service/ExternalApiService.php
namespace App\Service;
class ExternalApiService
{
public function fetchData(): array
{
// Führt einen externen API-Aufruf aus
// In Tests möchten wir dies vermeiden
}
}
Anpassung des Controllers
// src/Controller/CalculatorController.php
namespace App\Controller;
use App\Service\Calculator;
use App\Service\ExternalApiService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class CalculatorController extends AbstractController
{
#[Route('/external', name: 'external')]
public function externalData(ExternalApiService $apiService): Response
{
$data = $apiService->fetchData();
return new Response('Externe Daten: ' . json_encode($data));
}
}
Schreiben eines Tests mit einem Mock
// tests/Controller/CalculatorControllerTest.php
namespace App\Tests\Controller;
use App\Service\ExternalApiService;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class CalculatorControllerTest extends WebTestCase
{
public function testExternalData()
{
$client = static::createClient();
// Erstellen eines Mocks für ExternalApiService
$mockApiService = $this->createMock(ExternalApiService::class);
$mockApiService->method('fetchData')
->willReturn(['key' => 'value']);
// Überschreiben des Services im Container
$client->getContainer()->set(ExternalApiService::class, $mockApiService);
$client->request('GET', '/external');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('body', 'Externe Daten: {"key":"value"}');
}
}
5.3 Verwendung von Test-Doubles für Datenbankinteraktionen
Bei Unit-Tests möchten wir häufig echte Datenbankinteraktionen vermeiden. Hier können wir Mocks für Repositories erstellen.
Beispiel: Mocking eines Repositorys
// src/Repository/UserRepository.php
namespace App\Repository;
use App\Entity\User;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository
{
public function findActiveUsers(): array
{
return $this->createQueryBuilder('u')
->where('u.active = :active')
->setParameter('active', true)
->getQuery()
->getResult();
}
}
Test mit einem Mock-Repository
// tests/Service/UserServiceTest.php
namespace App\Tests\Service;
use App\Entity\User;
use App\Repository\UserRepository;
use PHPUnit\Framework\TestCase;
class UserServiceTest extends TestCase
{
public function testGetActiveUsers()
{
// Erstellen von Mock-Daten
$user1 = new User();
$user1->setEmail('user1@example.com');
$user2 = new User();
$user2->setEmail('user2@example.com');
// Erstellen eines Mocks für UserRepository
$mockRepository = $this->createMock(UserRepository::class);
$mockRepository->method('findActiveUsers')
->willReturn([$user1, $user2]);
// Testen der Methode, die das Repository verwendet
$users = $mockRepository->findActiveUsers();
$this->assertCount(2, $users);
$this->assertEquals('user1@example.com', $users[0]->getEmail());
}
}
Zusammenfassung
- Einrichtung von PHPUnit: Installieren und konfigurieren Sie PHPUnit und das Symfony Test Pack für Ihre Anwendung.
- Unit-Tests: Schreiben Sie Tests für einzelne Klassen und Methoden, um deren korrekte Funktion sicherzustellen.
- Funktionale Tests: Testen Sie das Verhalten Ihrer Anwendung auf höherer Ebene, z. B. Controller und Routen.
- Formulare und Validierung: Stellen Sie sicher, dass Ihre Formulare korrekt funktionieren und die Validierungsregeln greifen.
- Test-Doubles und Mocks: Verwenden Sie Mocks, um Abhängigkeiten zu isolieren und gezielt zu testen.
Weiterführende Ressourcen