Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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-pack

The recipe (chapter 6) installs PHPUnit AND creates phpunit.dist.xml and tests/bootstrap.php.

Unit tests vs. functional tests: the difference

Test typeProperties
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
tests/Service/ProjectStatsServiceTest.php
<?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 only

Important assertions at a glance

  • assertSame($expected, $actual) – strict equality (===), ALWAYS prefer over assertEquals().
  • 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/coverage

Produces 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.