Unit Tests With PHPUnit
Unit Tests With PHPUnit
~17 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Time for automated quality assurance – we'll test ProjectStatsService from chapter 33 IN ISOLATION, WITHOUT a real database.
Installing the test pack
composer require --dev symfony/test-packThe recipe (chapter 6) installs PHPUnit AND creates phpunit.dist.xml and tests/bootstrap.php.
Unit tests vs. functional tests: the difference
| Test type | Properties |
|---|---|
| Unit tests (this chapter) | Test ONE class IN ISOLATION, with NO database, NO HTTP kernel – FAST (milliseconds), dependencies get replaced with fakes/mocks. |
| Functional tests (chapter 43) | Test the INTERPLAY of several components via a REAL HTTP request – SLOWER, but more realistic. |
Writing the first unit test
php bin/console make:test TestCase ProjectStatsServiceTest<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\Entity\Project;
use App\Repository\TaskRepository;
use App\Service\ProjectStatsService;
use PHPUnit\Framework\TestCase;
class ProjectStatsServiceTest extends TestCase
{
public function testComputeStatsWithNoTasks(): void
{
$taskRepository = $this->createMock(TaskRepository::class);
$taskRepository->method('countTasksByStatus')->willReturn([]);
$service = new ProjectStatsService($taskRepository);
$result = $service->computeStats(new Project());
self::assertSame(0, $result['open']);
self::assertSame(0.0, $result['progress']);
}
public function testComputeStatsWithProgress(): void
{
$taskRepository = $this->createMock(TaskRepository::class);
$taskRepository->method('countTasksByStatus')->willReturn([
['status' => 'done', 'count' => 3],
['status' => 'open', 'count' => 1],
]);
$service = new ProjectStatsService($taskRepository);
$result = $service->computeStats(new Project());
self::assertSame(75.0, $result['progress']);
}
}createMock(TaskRepository::class) creates a FAKE repository that NEVER touches a real database – method(...)->willReturn(...) defines WHAT the fake method should return when called. EXACTLY THIS is the practical value of dependency injection from chapter 32: ProjectStatsService DOESN'T KNOW whether it's working with a real or a fake repository.
Running the tests
php bin/phpunit
php bin/phpunit tests/Service/ProjectStatsServiceTest.php # a single file only
php bin/phpunit --filter testComputeStatsWithProgress # a single method onlyImportant assertions at a glance
assertSame($expected, $actual)– strict equality (===), ALWAYS prefer overassertEquals().assertTrue(...)/assertFalse(...)– boolean check.assertCount($count, $array)– array/collection length.assertInstanceOf(Class::class, $object)– type check.expectException(Class::class)– expects a specific exception to be thrown.
public function testThrowsOnInvalidStatus(): void
{
$this->expectException(\InvalidArgumentException::class);
$task = new Task();
$task->setStatus('invalid-status');
}Measuring test coverage
php bin/phpunit --coverage-html var/coverageProduces an HTML report that shows, LINE BY LINE, which code is covered by tests – USEFUL for finding blind spots, but NOT an end in itself: 100% coverage does NOT automatically guarantee bug-free code, only that every line ran AT LEAST ONCE.
Tipp: Rule of thumb: unit tests are EXCELLENT for services with clear, isolatable logic like ProjectStatsService – for code heavily dependent on the database or HTTP context (controllers, repositories themselves), functional tests (chapter 43) are often the BETTER-FITTING approach.