Testing in PHP is een van die onderwerpen waar veel developers lang omheen lopen, totdat ze voor het eerst in een bestaand project iets wijzigen en per ongeluk iets anders slopen. Met een goede testsuite voorkom je dat soort verrassingen. Je schrijft kleine scripts die automatisch controleren of je code nog steeds doet wat je verwacht.
In dit artikel leer je hoe testing in PHP werkt, welke tools je gebruikt en hoe je stap voor stap je eerste unit tests schrijft met PHPUnit. We kijken naar assertions, mocking, test-driven development en hoe je testing integreert in je dagelijkse workflow.
Waarom testing belangrijk is
Software zonder tests is als een verbouwing zonder waterpas: misschien werkt het nu, maar je weet het pas zeker als alles instort. Tests geven je vertrouwen dat je code doet wat je denkt dat hij doet, ook over een half jaar, als een collega er iets aan wijzigt.
Goede tests hebben drie concrete voordelen. Ze vangen regressies op voordat ze in productie verschijnen. Ze fungeren als levende documentatie van hoe je code bedoeld is. En ze dwingen je tot een betere architectuur, omdat slecht opgezette code notoir lastig te testen is.
Vooral in grotere projecten met MVC-structuur of een uitgebreide API worden tests onmisbaar. Zonder tests wordt elke wijziging een gok.
Soorten tests in PHP
Niet elke test doet hetzelfde. Er zijn grofweg drie categorieën die je tegenkomt in PHP-projecten.
Unit tests
Unit tests testen één geïsoleerd stukje code, meestal één methode van een class. Ze draaien snel en hebben geen externe afhankelijkheden nodig zoals een database of API. Dit is het fundament van je testsuite.
Integration tests
Integration tests controleren of meerdere onderdelen samen werken. Denk aan een repository die daadwerkelijk query's uitvoert op een testdatabase, of een service die een externe API aanroept. Ze zijn langzamer dan unit tests, maar vangen bugs in de samenwerking tussen componenten.
End-to-end tests (E2E)
E2E tests simuleren een echte gebruiker die door je applicatie klikt. Ze testen het hele systeem, van HTTP-request tot database-response. Krachtig, maar traag en fragiel.
Een gezonde testsuite heeft veel unit tests, minder integration tests en een handvol E2E tests voor de belangrijkste gebruikersflows. Deze verhouding noem je de testpiramide.
PHPUnit installeren
PHPUnit is al jaren de standaard testtool voor PHP. Je installeert hem het eenvoudigst via Composer als development dependency:
composer require --dev phpunit/phpunit ^11
Daarna maak je een phpunit.xml configuratiebestand in de root van je project:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
</phpunit>
De map tests/ spiegelt meestal de structuur van je src/ map. Voor elke class in src/Services/UserService.php maak je een test in tests/Unit/Services/UserServiceTest.php.
Je eerste unit test schrijven
Laten we een eenvoudige class testen. Stel, je hebt een Calculator class:
<?php
namespace App\Services;
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('Delen door nul mag niet');
}
return $a / $b;
}
}
De bijbehorende test ziet er zo uit:
<?php
namespace Tests\Unit\Services;
use App\Services\Calculator;
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
public function test_it_adds_two_numbers(): void
{
$calculator = new Calculator();
$result = $calculator->add(2, 3);
$this->assertSame(5, $result);
}
public function test_it_throws_when_dividing_by_zero(): void
{
$calculator = new Calculator();
$this->expectException(\InvalidArgumentException::class);
$calculator->divide(10, 0);
}
}
Draai de tests met:
./vendor/bin/phpunit
Je ziet groene punten als alles slaagt, of een rode F (failure) of E (error) als er iets misgaat.
Het AAA-patroon
Let op de structuur in de tests hierboven: Arrange, Act, Assert. Dit patroon houdt je tests leesbaar.
- Arrange: zet de benodigde objecten en data klaar.
- Act: voer de actie uit die je wilt testen.
- Assert: controleer of het resultaat klopt.
Door deze drie stappen consequent te scheiden met een lege regel blijft elke test op één oogopslag te begrijpen, ook over een jaar.
Assertions die je vaak gebruikt
PHPUnit heeft tientallen assertion-methodes. Een paar die je bijna dagelijks nodig hebt:
assertSame($expected, $actual), strikte vergelijking (type én waarde).assertEquals($expected, $actual), losse vergelijking (alleen waarde).assertTrue($condition)enassertFalse($condition), booleans controleren.assertNull($value)enassertNotNull($value), null-checks.assertCount($count, $array), aantal elementen in een array.assertInstanceOf(ClassName::class, $object), type-controle.expectException(Exception::class), verwachte uitzondering.
Gebruik assertSame als default. Het vangt stilzwijgende type-coercions op die je anders mist.
Mocks en dependencies
Zodra je class afhankelijkheden heeft, wordt testen lastiger. Stel, je OrderService gebruikt een MailerInterface om bevestigingsmails te sturen. In een unit test wil je geen echte mail versturen.
Hiervoor gebruik je mocks: neppe objecten die dezelfde interface hebben als het echte object, maar die jij kunt configureren.
public function test_it_sends_confirmation_after_placing_order(): void
{
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects($this->once())
->method('send')
->with($this->stringContains('order-bevestiging'));
$service = new OrderService($mailer);
$service->placeOrder(['product_id' => 42]);
}
Je verifieert hier twee dingen: dat send() precies één keer wordt aangeroepen, en dat de inhoud de tekst "order-bevestiging" bevat. Het echte verzenden gebeurt nooit.
Let op: als je te veel mocks nodig hebt, is dat vaak een teken dat je class te veel verantwoordelijkheden heeft. Kleinere, gefocuste classes zijn makkelijker te testen.
Databases testen
Voor code die praat met een database via PDO heb je twee opties. Je kunt de PDO-laag mocken, maar dat levert vaak brosse tests op. Beter is om een aparte testdatabase te gebruiken.
Een veelgebruikte aanpak is een in-memory SQLite-database tijdens tests:
protected function setUp(): void
{
$this->pdo = new \PDO('sqlite::memory:');
$this->pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)');
}
De setUp() methode draait voor elke test opnieuw, zodat je altijd met een schone lei begint. Er is ook een tearDown() voor opruimen achteraf.
Test-driven development
Bij test-driven development (TDD) draai je de volgorde om: je schrijft eerst de test, daarna de code. De cyclus heet red-green-refactor.
- Red: schrijf een test die faalt (de feature bestaat nog niet).
- Green: schrijf de minimale code om de test te laten slagen.
- Refactor: verbeter de code zonder de tests te breken.
TDD voelt in het begin traag, maar dwingt je klein en doelgericht te werken. Je schrijft alleen code die echt nodig is, en je hebt per definitie een complete testsuite als je klaar bent. Kent Beck's boek Test-Driven Development: By Example is nog steeds de klassieker als je dieper wilt duiken.
Code coverage interpreteren
PHPUnit kan een rapport genereren dat laat zien welke regels code door tests worden uitgevoerd:
./vendor/bin/phpunit --coverage-html coverage/
Coverage-percentages zijn verleidelijk als KPI, maar misleidend. Honderd procent coverage betekent alleen dat elke regel is uitgevoerd, niet dat elk scenario is getest. Richt je liever op kritieke paden: authenticatieflows, validatie en security, en business logic die geld of data raakt.
Testing en CI/CD
Tests zijn pas echt nuttig als ze automatisch draaien bij elke commit. Koppel PHPUnit aan je CI-pipeline (GitHub Actions, GitLab CI, of iets anders). Een pull request die tests breekt, zou niet gemerged mogen worden.
Combineer dit met statische analyse-tools zoals PHPStan of Psalm en je vangt een enorme categorie bugs af voordat je code ooit in productie draait.
Alternatieven: Pest
Naast PHPUnit is Pest de afgelopen jaren populair geworden. Pest biedt een compactere, expressievere syntax:
it('adds two numbers', function () {
expect((new Calculator())->add(2, 3))->toBe(5);
});
Onder de motorkap draait Pest op PHPUnit, dus je kunt ze zelfs mixen. Welke je kiest is persoonlijk: PHPUnit is explicieter en breder ondersteund, Pest is beknopter en prettig leesbaar.
Veelgemaakte fouten
Een paar valkuilen die je beter meteen kunt vermijden:
- Tests die afhankelijk zijn van andere tests: elke test moet zelfstandig kunnen draaien in willekeurige volgorde.
- Te veel mocks: als je mocks bijna een copy-paste van de echte class worden, test je niets zinnigs meer.
- Tijdsafhankelijke tests: vermijd
sleep()of checks tegentime()zonder een injecteerbare clock. - Tests die implementatie testen in plaats van gedrag: test wat je code doet, niet hoe. Anders breken je tests bij elke refactor.
Veelgestelde vragen
Wat is testing in PHP?
Testing in PHP betekent dat je automatische scripts schrijft die controleren of je code doet wat jij verwacht. Met tools zoals PHPUnit voer je deze tests uit en zie je direct of er iets kapot gaat na een wijziging.
Wat is het verschil tussen unit tests en integration tests?
Unit tests testen één klein stukje code in isolatie, bijvoorbeeld één methode van een class. Integration tests controleren of meerdere onderdelen samen goed werken, zoals een database-query via je repository.
Is PHPUnit de beste tool voor testing in PHP?
PHPUnit is de meest gebruikte en best gedocumenteerde testtool voor PHP en wordt door vrijwel elk framework ondersteund. Alternatieven zoals Pest zijn populair vanwege hun leesbare syntax, maar draaien onder de motorkap vaak op PHPUnit.
Wat is test-driven development (TDD)?
Bij TDD schrijf je eerst een test die faalt, daarna de minimale code om die test te laten slagen, en tot slot refactor je de code. Deze cyclus noem je red-green-refactor en leidt vaak tot betere softwarearchitectuur.
Hoeveel tests heb ik nodig in mijn project?
Er is geen vast getal, maar richt je op de belangrijkste logica en edge cases in plaats van op een percentage code coverage. Liever 50 goede tests dan 500 tests die niets zinvols controleren.
Conclusie
Testing in PHP is geen luxe, maar een basisvaardigheid die je werk als developer significant verbetert. Met PHPUnit heb je een volwassen tool die in elk serieus project thuishoort. Begin klein: schrijf tests voor je nieuwe features, voeg tests toe wanneer je een bug fixt, en bouw je suite geleidelijk uit.
Na een paar weken merk je dat je met meer vertrouwen code wijzigt en dat je minder tijd kwijt bent aan handmatig klikken in je browser om te checken of iets nog werkt. Dat vertrouwen, dát is de echte winst van testing.