MidТеория12 min

Основы PHPUnit

Установка, конфигурация, assertions, lifecycle methods, data providers, исключения

Что такое PHPUnit

PHPUnit — это фреймворк для модульного тестирования PHP-кода. Он является стандартом де-факто для тестирования в PHP-экосистеме. PHPUnit позволяет писать автоматические тесты, которые проверяют корректность работы отдельных модулей (классов, методов, функций).

Зачем нужно тестирование:

  • Уверенность в коде — тесты доказывают, что код работает правильно
  • Безопасный рефакторинг — тесты ловят регрессии при изменениях
  • Документация — тесты показывают, как использовать код
  • Быстрая обратная связь — не нужно вручную проверять каждое изменение
  • Дизайн кода — написание тестов заставляет проектировать тестируемый код

Установка и настройка

Установка через Composer

# Install PHPUnit as dev dependency
composer require --dev phpunit/phpunit

# Verify installation
vendor/bin/phpunit --version
# PHPUnit 11.5.x by Sebastian Bergmann and contributors.

Конфигурация phpunit.xml

Создайте файл phpunit.xml (или phpunit.xml.dist) в корне проекта:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         stopOnFailure="false"
         cacheDirectory=".phpunit.cache"
         executionOrder="random"
         failOnRisky="true"
         failOnWarning="true">

    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory>tests/Integration</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory>src</directory>
        </include>
        <exclude>
            <directory>src/DataFixtures</directory>
        </exclude>
    </source>

    <coverage>
        <report>
            <html outputDirectory="coverage"/>
            <clover outputFile="coverage/clover.xml"/>
        </report>
    </coverage>

    <php>
        <env name="APP_ENV" value="test"/>
        <env name="DATABASE_URL" value="sqlite:///:memory:"/>
    </php>
</phpunit>

Структура директорий

project/
├── src/
│   ├── Entity/
│   │   └── User.php
│   ├── Service/
│   │   └── UserService.php
│   └── Repository/
│       └── UserRepository.php
├── tests/
│   ├── Unit/
│   │   ├── Entity/
│   │   │   └── UserTest.php
│   │   └── Service/
│   │       └── UserServiceTest.php
│   └── Integration/
│       └── Repository/
│           └── UserRepositoryTest.php
├── phpunit.xml
└── composer.json

Первый тест

Класс для тестирования

<?php

declare(strict_types=1);

namespace App\Service;

/**
 * Simple calculator for arithmetic operations.
 */
final class Calculator
{
    public function add(float $a, float $b): float
    {
        return $a + $b;
    }

    public function subtract(float $a, float $b): float
    {
        return $a - $b;
    }

    public function multiply(float $a, float $b): float
    {
        return $a * $b;
    }

    public function divide(float $a, float $b): float
    {
        if ($b === 0.0) {
            throw new \DivisionByZeroError('Division by zero');
        }

        return $a / $b;
    }

    public function percentage(float $value, float $percent): float
    {
        return $value * ($percent / 100);
    }
}

Тестовый класс

<?php

declare(strict_types=1);

namespace App\Tests\Unit\Service;

use App\Service\Calculator;
use PHPUnit\Framework\TestCase;

/**
 * @covers \App\Service\Calculator
 */
final class CalculatorTest extends TestCase
{
    private Calculator $calculator;

    protected function setUp(): void
    {
        $this->calculator = new Calculator();
    }

    public function testAddReturnsSumOfTwoNumbers(): void
    {
        $result = $this->calculator->add(2, 3);

        self::assertSame(5.0, $result);
    }

    public function testSubtractReturnsDifference(): void
    {
        $result = $this->calculator->subtract(10, 4);

        self::assertSame(6.0, $result);
    }

    public function testMultiplyReturnsProduct(): void
    {
        $result = $this->calculator->multiply(3, 7);

        self::assertSame(21.0, $result);
    }

    public function testDivideReturnsDivision(): void
    {
        $result = $this->calculator->divide(10, 2);

        self::assertSame(5.0, $result);
    }

    public function testDivideByZeroThrowsException(): void
    {
        $this->expectException(\DivisionByZeroError::class);
        $this->expectExceptionMessage('Division by zero');

        $this->calculator->divide(10, 0);
    }

    public function testPercentageCalculation(): void
    {
        $result = $this->calculator->percentage(200, 15);

        self::assertSame(30.0, $result);
    }
}

Запуск тестов

# Run all tests
vendor/bin/phpunit

# Run specific test file
vendor/bin/phpunit tests/Unit/Service/CalculatorTest.php

# Run specific test method
vendor/bin/phpunit --filter testAddReturnsSumOfTwoNumbers

# Run specific test suite
vendor/bin/phpunit --testsuite Unit

# Run with verbose output
vendor/bin/phpunit -v

# Run and stop on first failure
vendor/bin/phpunit --stop-on-failure

Именование тестов

Два способа объявить метод как тест:

<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class NamingConventionTest extends TestCase
{
    // Method 1: prefix with "test"
    public function testSomethingWorksCorrectly(): void
    {
        self::assertTrue(true);
    }

    // Method 2: #[Test] attribute (PHP 8.1+)
    #[Test]
    public function somethingWorksCorrectly(): void
    {
        self::assertTrue(true);
    }
}

Рекомендация: используйте префикс test — это явнее и читаемее. Названия должны описывать поведение: testUserCannotLoginWithInvalidPassword.

Assertions — утверждения

Assertions — это проверки, которые определяют успешность теста. PHPUnit предоставляет десятки встроенных assertions.

Базовые assertions

<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use PHPUnit\Framework\TestCase;

final class AssertionExamplesTest extends TestCase
{
    // Equality
    public function testEquality(): void
    {
        // assertEquals: loose comparison (==)
        self::assertEquals('123', 123);   // passes (type juggling)
        self::assertEquals(0, false);      // passes

        // assertSame: strict comparison (===)
        self::assertSame(123, 123);        // passes
        // self::assertSame('123', 123);   // FAILS (different types)

        // assertNotEquals / assertNotSame
        self::assertNotEquals('abc', 'xyz');
        self::assertNotSame(1, '1');
    }

    // Boolean
    public function testBoolean(): void
    {
        self::assertTrue(1 === 1);
        self::assertFalse(1 === 2);
    }

    // Null
    public function testNull(): void
    {
        self::assertNull(null);
        self::assertNotNull('value');
    }

    // Type checking
    public function testTypes(): void
    {
        self::assertIsString('hello');
        self::assertIsInt(42);
        self::assertIsFloat(3.14);
        self::assertIsBool(true);
        self::assertIsArray([1, 2, 3]);
        self::assertIsObject(new \stdClass());
    }

    // Instance checking
    public function testInstanceOf(): void
    {
        $exception = new \InvalidArgumentException('test');

        self::assertInstanceOf(\InvalidArgumentException::class, $exception);
        self::assertInstanceOf(\Exception::class, $exception);  // parent class
        self::assertInstanceOf(\Throwable::class, $exception);  // interface
    }
}

Assertions для строк

<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use PHPUnit\Framework\TestCase;

final class StringAssertionsTest extends TestCase
{
    public function testStringContains(): void
    {
        $message = 'Hello, World!';

        self::assertStringContainsString('World', $message);
        self::assertStringNotContainsString('Goodbye', $message);

        // Case-insensitive
        self::assertStringContainsStringIgnoringCase('world', $message);
    }

    public function testStringStartsAndEnds(): void
    {
        $url = 'https://example.com/api/users';

        self::assertStringStartsWith('https://', $url);
        self::assertStringEndsWith('/users', $url);
    }

    public function testRegularExpression(): void
    {
        $email = '[email protected]';

        self::assertMatchesRegularExpression('/^[a-z]+@[a-z]+\.[a-z]+$/i', $email);
    }

    public function testJson(): void
    {
        $expected = '{"name":"John","age":30}';
        $actual = '{"age":30,"name":"John"}';

        // Compares JSON structures (ignores key order)
        self::assertJsonStringEqualsJsonString($expected, $actual);
    }

    public function testEmpty(): void
    {
        self::assertEmpty('');
        self::assertEmpty([]);
        self::assertNotEmpty('text');
    }
}

Assertions для массивов

<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use PHPUnit\Framework\TestCase;

final class ArrayAssertionsTest extends TestCase
{
    public function testArrayContains(): void
    {
        $fruits = ['apple', 'banana', 'cherry'];

        self::assertContains('banana', $fruits);
        self::assertNotContains('grape', $fruits);
        self::assertCount(3, $fruits);
    }

    public function testArrayHasKey(): void
    {
        $user = [
            'name' => 'John',
            'email' => '[email protected]',
            'age' => 30,
        ];

        self::assertArrayHasKey('name', $user);
        self::assertArrayHasKey('email', $user);
        self::assertArrayNotHasKey('password', $user);
    }

    public function testArrayEquality(): void
    {
        $expected = [1, 2, 3];
        $actual = [1, 2, 3];

        self::assertSame($expected, $actual);     // Same order and types
        self::assertEquals($expected, $actual);    // Same values (loose)
    }

    public function testContainsOnly(): void
    {
        $numbers = [1, 2, 3, 4, 5];

        self::assertContainsOnly('int', $numbers);
    }
}

Assertions для сравнений

<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use PHPUnit\Framework\TestCase;

final class ComparisonAssertionsTest extends TestCase
{
    public function testGreaterAndLess(): void
    {
        self::assertGreaterThan(5, 10);
        self::assertGreaterThanOrEqual(5, 5);
        self::assertLessThan(10, 5);
        self::assertLessThanOrEqual(5, 5);
    }

    public function testEqualsWithDelta(): void
    {
        // Useful for floating-point comparison
        self::assertEqualsWithDelta(3.14, 3.141592, 0.01);
    }
}

Lifecycle methods — методы жизненного цикла

PHPUnit предоставляет хуки, которые выполняются в определённые моменты жизненного цикла теста.

<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use PHPUnit\Framework\TestCase;

final class LifecycleTest extends TestCase
{
    private static int $connectionCount = 0;

    /**
     * Runs ONCE before ALL tests in this class.
     * Use for expensive setup (database connection, fixtures).
     */
    public static function setUpBeforeClass(): void
    {
        self::$connectionCount++;
        // e.g., create database schema, load fixtures
    }

    /**
     * Runs BEFORE EACH test method.
     * Use for per-test setup (fresh objects, reset state).
     */
    protected function setUp(): void
    {
        // Create fresh Calculator for each test
        // $this->calculator = new Calculator();
    }

    /**
     * Runs AFTER EACH test method.
     * Use for cleanup (close connections, delete temp files).
     */
    protected function tearDown(): void
    {
        // Cleanup after each test
    }

    /**
     * Runs ONCE after ALL tests in this class.
     * Use for global cleanup.
     */
    public static function tearDownAfterClass(): void
    {
        // e.g., drop database, close connection pool
    }

    public function testFirst(): void
    {
        // setUp() ran before this
        self::assertTrue(true);
        // tearDown() runs after this
    }

    public function testSecond(): void
    {
        // setUp() ran again before this
        self::assertTrue(true);
        // tearDown() runs after this
    }
}

Порядок выполнения

setUpBeforeClass()       ← один раз перед всеми тестами
  setUp()                ← перед testFirst
    testFirst()
  tearDown()             ← после testFirst
  setUp()                ← перед testSecond
    testSecond()
  tearDown()             ← после testSecond
tearDownAfterClass()     ← один раз после всех тестов

Практический пример

<?php

declare(strict_types=1);

namespace App\Tests\Unit\Service;

use App\Entity\User;
use App\Service\UserValidator;
use PHPUnit\Framework\TestCase;

final class UserValidatorTest extends TestCase
{
    private UserValidator $validator;

    protected function setUp(): void
    {
        // Fresh validator for each test — no shared state
        $this->validator = new UserValidator();
    }

    public function testValidEmailPasses(): void
    {
        $user = new User(
            name: 'John Doe',
            email: '[email protected]',
        );

        $result = $this->validator->validate($user);

        self::assertTrue($result->isValid());
        self::assertEmpty($result->getErrors());
    }

    public function testInvalidEmailFails(): void
    {
        $user = new User(
            name: 'John Doe',
            email: 'not-an-email',
        );

        $result = $this->validator->validate($user);

        self::assertFalse($result->isValid());
        self::assertArrayHasKey('email', $result->getErrors());
    }
}

Data Providers

Data providers позволяют запускать один тестовый метод с разными наборами данных. Это устраняет дублирование тестов.

Базовый Data Provider

<?php

declare(strict_types=1);

namespace App\Tests\Unit\Service;

use App\Service\Calculator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class CalculatorDataProviderTest extends TestCase
{
    private Calculator $calculator;

    protected function setUp(): void
    {
        $this->calculator = new Calculator();
    }

    /**
     * @return iterable<string, array{float, float, float}>
     */
    public static function additionProvider(): iterable
    {
        yield 'positive numbers' => [2.0, 3.0, 5.0];
        yield 'negative numbers' => [-1.0, -2.0, -3.0];
        yield 'mixed numbers' => [-1.0, 3.0, 2.0];
        yield 'zeros' => [0.0, 0.0, 0.0];
        yield 'decimals' => [0.1, 0.2, 0.3];
        yield 'large numbers' => [1_000_000.0, 2_000_000.0, 3_000_000.0];
    }

    #[DataProvider('additionProvider')]
    public function testAdd(float $a, float $b, float $expected): void
    {
        $result = $this->calculator->add($a, $b);

        self::assertEqualsWithDelta($expected, $result, 0.0001);
    }

    /**
     * @return iterable<string, array{float, float, float}>
     */
    public static function divisionProvider(): iterable
    {
        yield 'simple division' => [10.0, 2.0, 5.0];
        yield 'decimal result' => [7.0, 2.0, 3.5];
        yield 'divide by one' => [42.0, 1.0, 42.0];
        yield 'negative dividend' => [-10.0, 2.0, -5.0];
        yield 'both negative' => [-10.0, -2.0, 5.0];
    }

    #[DataProvider('divisionProvider')]
    public function testDivide(float $a, float $b, float $expected): void
    {
        $result = $this->calculator->divide($a, $b);

        self::assertEqualsWithDelta($expected, $result, 0.0001);
    }
}

Data Provider с объектами

<?php

declare(strict_types=1);

namespace App\Tests\Unit\Entity;

use App\Entity\User;
use App\Enum\UserRole;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class UserTest extends TestCase
{
    /**
     * @return iterable<string, array{string, string, UserRole, bool}>
     */
    public static function validUserProvider(): iterable
    {
        yield 'regular user' => [
            'John Doe',
            '[email protected]',
            UserRole::User,
            false,
        ];

        yield 'admin user' => [
            'Admin',
            '[email protected]',
            UserRole::Admin,
            true,
        ];

        yield 'moderator' => [
            'Mod User',
            '[email protected]',
            UserRole::Moderator,
            false,
        ];
    }

    #[DataProvider('validUserProvider')]
    public function testUserCreation(
        string $name,
        string $email,
        UserRole $role,
        bool $expectedAdmin,
    ): void {
        $user = new User(
            name: $name,
            email: $email,
            role: $role,
        );

        self::assertSame($name, $user->getName());
        self::assertSame($email, $user->getEmail());
        self::assertSame($role, $user->getRole());
        self::assertSame($expectedAdmin, $user->isAdmin());
    }
}

Generator-based Data Provider

<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class EmailValidationTest extends TestCase
{
    /**
     * @return \Generator<string, array{string, bool}>
     */
    public static function emailProvider(): \Generator
    {
        // Valid emails
        yield 'simple email' => ['[email protected]', true];
        yield 'with dots' => ['[email protected]', true];
        yield 'with plus' => ['[email protected]', true];
        yield 'subdomain' => ['[email protected]', true];

        // Invalid emails
        yield 'no at sign' => ['user-example.com', false];
        yield 'no domain' => ['user@', false];
        yield 'no local part' => ['@example.com', false];
        yield 'spaces' => ['user @example.com', false];
        yield 'empty string' => ['', false];
    }

    #[DataProvider('emailProvider')]
    public function testEmailValidation(string $email, bool $expectedValid): void
    {
        $isValid = filter_var($email, FILTER_VALIDATE_EMAIL) !== false;

        self::assertSame($expectedValid, $isValid);
    }
}

Тестирование исключений

Базовое тестирование исключений

<?php

declare(strict_types=1);

namespace App\Tests\Unit\Service;

use App\Service\UserService;
use App\Exception\UserNotFoundException;
use App\Exception\InvalidEmailException;
use PHPUnit\Framework\TestCase;

final class UserServiceExceptionTest extends TestCase
{
    private UserService $service;

    protected function setUp(): void
    {
        $this->service = new UserService();
    }

    public function testThrowsExceptionForInvalidEmail(): void
    {
        $this->expectException(InvalidEmailException::class);
        $this->expectExceptionMessage('Email "not-valid" is not a valid email address');
        $this->expectExceptionCode(422);

        $this->service->createUser('John', 'not-valid');
    }

    public function testThrowsNotFoundForUnknownUser(): void
    {
        $this->expectException(UserNotFoundException::class);
        $this->expectExceptionMessageMatches('/User .+ not found/');

        $this->service->findUserOrFail('nonexistent-id');
    }
}

Тестирование с try-catch (для дополнительных проверок)

<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use App\Exception\ValidationException;
use App\Service\OrderService;
use PHPUnit\Framework\TestCase;

final class OrderServiceTest extends TestCase
{
    public function testInvalidOrderThrowsValidationException(): void
    {
        $service = new OrderService();

        try {
            $service->createOrder(items: [], customerId: '');
            self::fail('Expected ValidationException was not thrown');
        } catch (ValidationException $e) {
            self::assertSame(422, $e->getCode());
            self::assertCount(2, $e->getErrors());
            self::assertArrayHasKey('items', $e->getErrors());
            self::assertArrayHasKey('customerId', $e->getErrors());
        }
    }
}

Группировка и пропуск тестов

Группировка

<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;

#[Group('user')]
final class UserFeatureTest extends TestCase
{
    #[Group('auth')]
    public function testLogin(): void
    {
        self::assertTrue(true);
    }

    #[Group('profile')]
    public function testUpdateProfile(): void
    {
        self::assertTrue(true);
    }
}
# Run tests in specific group
vendor/bin/phpunit --group auth

# Exclude a group
vendor/bin/phpunit --exclude-group slow

Пропуск тестов

<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\TestCase;

final class ConditionalTest extends TestCase
{
    public function testSkippedInCi(): void
    {
        if (getenv('CI') !== false) {
            self::markTestSkipped('This test cannot run in CI');
        }

        // Test logic here
        self::assertTrue(true);
    }

    public function testIncomplete(): void
    {
        self::markTestIncomplete('Not yet implemented — see TASK-123');
    }

    #[RequiresPhpExtension('redis')]
    public function testRedisConnection(): void
    {
        // This test only runs when ext-redis is available
        self::assertTrue(true);
    }
}

Тестирование вывода

<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use App\Command\GreetCommand;
use PHPUnit\Framework\TestCase;

final class OutputTest extends TestCase
{
    public function testGreetOutputsCorrectMessage(): void
    {
        $this->expectOutputString("Hello, World!\n");

        $command = new GreetCommand();
        $command->execute('World');
    }

    public function testOutputContainsName(): void
    {
        $this->expectOutputRegex('/Hello, [A-Z][a-z]+!/');

        $command = new GreetCommand();
        $command->execute('John');
    }
}

Практический пример: тестирование UserService

Сервис

<?php

declare(strict_types=1);

namespace App\Service;

use App\Entity\User;
use App\Repository\UserRepositoryInterface;
use App\Exception\UserAlreadyExistsException;
use App\Exception\InvalidEmailException;

final readonly class UserService
{
    public function __construct(
        private UserRepositoryInterface $repository,
    ) {}

    /**
     * @throws InvalidEmailException
     * @throws UserAlreadyExistsException
     */
    public function register(string $name, string $email): User
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidEmailException(
                sprintf('Email "%s" is not valid', $email),
            );
        }

        if ($this->repository->findByEmail($email) !== null) {
            throw new UserAlreadyExistsException(
                sprintf('User with email "%s" already exists', $email),
            );
        }

        $user = new User(
            name: $name,
            email: $email,
        );

        $this->repository->save($user);

        return $user;
    }

    /**
     * @return list<User>
     */
    public function findActiveUsers(): array
    {
        return $this->repository->findByActive(true);
    }
}

Полный тестовый класс

<?php

declare(strict_types=1);

namespace App\Tests\Unit\Service;

use App\Entity\User;
use App\Exception\InvalidEmailException;
use App\Exception\UserAlreadyExistsException;
use App\Repository\UserRepositoryInterface;
use App\Service\UserService;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

/**
 * @covers \App\Service\UserService
 */
final class UserServiceTest extends TestCase
{
    private UserRepositoryInterface&MockObject $repository;
    private UserService $service;

    protected function setUp(): void
    {
        $this->repository = $this->createMock(UserRepositoryInterface::class);
        $this->service = new UserService($this->repository);
    }

    public function testRegisterCreatesNewUser(): void
    {
        $this->repository
            ->method('findByEmail')
            ->with('[email protected]')
            ->willReturn(null);

        $this->repository
            ->expects(self::once())
            ->method('save')
            ->with(self::isInstanceOf(User::class));

        $user = $this->service->register('John', '[email protected]');

        self::assertSame('John', $user->getName());
        self::assertSame('[email protected]', $user->getEmail());
    }

    public function testRegisterThrowsForInvalidEmail(): void
    {
        $this->expectException(InvalidEmailException::class);
        $this->expectExceptionMessage('"not-an-email" is not valid');

        $this->service->register('John', 'not-an-email');
    }

    public function testRegisterThrowsForDuplicateEmail(): void
    {
        $existingUser = new User(name: 'Existing', email: '[email protected]');

        $this->repository
            ->method('findByEmail')
            ->with('[email protected]')
            ->willReturn($existingUser);

        $this->expectException(UserAlreadyExistsException::class);

        $this->service->register('John', '[email protected]');
    }

    /**
     * @return iterable<string, array{string}>
     */
    public static function invalidEmailProvider(): iterable
    {
        yield 'no at sign' => ['invalid'];
        yield 'no domain' => ['user@'];
        yield 'no local' => ['@domain.com'];
        yield 'spaces' => ['user @domain.com'];
        yield 'empty' => [''];
    }

    #[DataProvider('invalidEmailProvider')]
    public function testRegisterRejectsInvalidEmails(string $email): void
    {
        $this->expectException(InvalidEmailException::class);

        $this->service->register('Test', $email);
    }

    public function testFindActiveUsersReturnsUsers(): void
    {
        $users = [
            new User(name: 'Alice', email: '[email protected]'),
            new User(name: 'Bob', email: '[email protected]'),
        ];

        $this->repository
            ->method('findByActive')
            ->with(true)
            ->willReturn($users);

        $result = $this->service->findActiveUsers();

        self::assertCount(2, $result);
        self::assertSame('Alice', $result[0]->getName());
        self::assertSame('Bob', $result[1]->getName());
    }

    public function testFindActiveUsersReturnsEmptyWhenNoUsers(): void
    {
        $this->repository
            ->method('findByActive')
            ->willReturn([]);

        $result = $this->service->findActiveUsers();

        self::assertEmpty($result);
    }
}

Проверь себя

5 из 10

Как запустить только тесты определённой группы?

Как проверить, что метод выбрасывает определённое исключение?

Как правильно настроить bootstrap в phpunit.xml?

Какой assertion проверяет наличие ключа в ассоциативном массиве?

Чем отличается assertEquals от assertSame?