PHP: Тестирование

17 вопросов

1 Чем unit-тесты отличаются от интеграционных?

Unit-тесты - тестируют отдельный класс/метод в изоляции (зависимости замокированы). Быстрые, детерминированные.

Интеграционные - тестируют взаимодействие нескольких компонентов (БД, API, файлы). Медленнее, но проверяют реальную работу.

// Unit test
public function testCalculateDiscount(): void {
    $calc = new DiscountCalculator();
    $this->assertEquals(10.0, $calc->calculate(100.0, 10));
}

// Integration test
public function testCreateOrder(): void {
    $order = $this->orderService->create($this->user, $this->items);
    $this->assertDatabaseHas('orders', ['user_id' => $this->user->id]);
}

Пирамида тестирования: много unit, меньше integration, мало E2E.

Открыть отдельно →
2 Что такое PHPUnit?

PHPUnit - стандартный фреймворк для тестирования PHP. Создан Себастьяном Бергманном.

use PHPUnit\Framework\TestCase;

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

    public function testCreateUser(): void {
        $user = $this->service->create('John', 'john@mail.com');
        $this->assertInstanceOf(User::class, $user);
        $this->assertEquals('John', $user->name);
    }

    #[DataProvider('emailProvider')]
    public function testValidateEmail(string $email, bool $expected): void {
        $this->assertEquals($expected, $this->service->validateEmail($email));
    }

    public static function emailProvider(): array {
        return [
            ['test@mail.com', true],
            ['invalid', false],
        ];
    }
}
Открыть отдельно →
3 Что такое Pest?

Pest - тестовый фреймворк поверх PHPUnit с лаконичным синтаксисом:

// Pest
test('user can be created', function () {
    $user = User::factory()->create();
    expect($user)->toBeInstanceOf(User::class)
        ->name->not->toBeEmpty()
        ->email->toContain('@');
});

it('validates email', function (string $email, bool $valid) {
    expect(isValidEmail($email))->toBe($valid);
})->with([
    ['test@mail.com', true],
    ['invalid', false],
]);

Pest совместим с PHPUnit - можно миксовать стили. Менее verbose, лучше читается.

Открыть отдельно →
4 Что такое test doubles?
  • Dummy - заглушка, передается но не используется
  • Stub - возвращает предопределенные данные
  • Mock - проверяет, что определенные методы были вызваны (верификация поведения)
  • Spy - записывает вызовы для последующей проверки
  • Fake - рабочая реализация, упрощенная для тестов (in-memory repository)
// Stub
$repo = $this->createStub(UserRepository::class);
$repo->method('find')->willReturn(new User('John'));

// Mock
$mailer = $this->createMock(Mailer::class);
$mailer->expects($this->once())
       ->method('send')
       ->with('john@mail.com', $this->anything());
Открыть отдельно →
5 Чем mock отличается от stub?

Stub - предоставляет заранее определенные ответы. Не проверяет вызовы. Тест проверяет состояние.

Mock - проверяет, что определенные методы были вызваны с правильными аргументами. Тест проверяет поведение.

// Stub - проверяем результат (state verification)
$repo = $this->createStub(UserRepo::class);
$repo->method('find')->willReturn($user);
$result = $service->getProfile(1);
$this->assertEquals('John', $result->name); // проверяем состояние

// Mock - проверяем взаимодействие (behavior verification)
$mailer = $this->createMock(Mailer::class);
$mailer->expects($this->once())->method('send'); // проверяем вызов
$service->register($user);

Предпочитайте stubs - тесты менее хрупкие при рефакторинге.

Открыть отдельно →
6 Что такое Mockery?

Mockery - альтернативная библиотека для мокирования, более гибкая чем встроенные PHPUnit mocks:

use Mockery;

$mock = Mockery::mock(UserRepository::class);
$mock->shouldReceive('find')
     ->with(42)
     ->once()
     ->andReturn(new User('John'));

$mock->shouldReceive('save')
     ->andReturnUsing(fn(User $u) => $u);

// Partial mock
$mock = Mockery::mock(Service::class)->makePartial();
$mock->shouldReceive('externalCall')->andReturn('cached');

Mockery::close(); // в tearDown

Mockery поддерживает: partial mocks, spy, named mocks, expectation declarations, hamcrest matchers.

Открыть отдельно →
7 Когда использовать mock, а когда fake?

Mock - когда нужно проверить взаимодействие (вызван ли метод, с какими аргументами):

$notifier->expects($this->once())->method('notify');

Fake - когда нужна рабочая реализация без внешних зависимостей:

class InMemoryUserRepository implements UserRepository {
    private array $users = [];
    public function save(User $user): void {
        $this->users[$user->id] = $user;
    }
    public function find(int $id): ?User {
        return $this->users[$id] ?? null;
    }
}

Fakes предпочтительнее для репозиториев, кешей, очередей - тесты проверяют бизнес-логику, а не конкретные вызовы. Mocks - для side effects (отправка email, API-вызов).

Открыть отдельно →
8 Что такое TDD?

TDD (Test-Driven Development) - разработка через тестирование. Цикл Red-Green-Refactor:

  1. Red - написать падающий тест
  2. Green - написать минимальный код, чтобы тест прошел
  3. Refactor - улучшить код, тесты по-прежнему проходят

Цель: не 100% покрытие, а дизайн кода через тесты. TDD приводит к лучшей архитектуре: маленькие классы, четкие интерфейсы, слабая связанность.

Открыть отдельно →
9 Какое покрытие тестами считается нормальным?

Нет универсального числа, но ориентиры:

  • 70-80% - хорошее покрытие для большинства проектов
  • 90%+ - для критичного кода (финансы, медицина)
  • 100% - часто нецелесообразно (getter/setter, framework glue code)

Важнее quality, чем quantity: покрытие бизнес-логики и edge cases важнее покрытия boilerplate. Mutation testing (Infection) - лучшая метрика качества тестов, чем code coverage.

Открыть отдельно →
10 Что такое mutation testing?

Mutation testing - проверка качества тестов путем внесения мутаций в код:

# Infection (PHP mutation testing framework)
composer require --dev infection/infection
vendor/bin/infection --min-msi=70

Infection изменяет код (меняет + на -, true на false, > на >=) и проверяет, ловят ли тесты эти изменения. Метрика MSI (Mutation Score Indicator) показывает процент "убитых" мутантов. Если мутант "выживает" - тесты недостаточно проверяют логику.

Открыть отдельно →
11 Что такое Codeception?

Codeception - фреймворк для BDD-тестирования, объединяющий unit, functional и acceptance тесты:

// Acceptance test (browser)
$I->amOnPage('/login');
$I->fillField('email', 'user@test.com');
$I->fillField('password', 'secret');
$I->click('Login');
$I->see('Welcome');

// Functional test (без браузера)
$I->sendPOST('/api/users', ['name' => 'John']);
$I->seeResponseCodeIs(201);
$I->seeResponseContainsJson(['name' => 'John']);

Codeception использует WebDriver для acceptance-тестов (реальный браузер), встроенные модули для Laravel, Symfony, REST API.

Открыть отдельно →
12 Что такое snapshot testing?

Snapshot testing - сохранение "эталонного" вывода и сравнение при последующих запусках:

// Пакет spatie/phpunit-snapshot-assertions
public function testRenderPage(): void {
    $html = $this->renderer->render('home');
    $this->assertMatchesSnapshot($html);
    // Первый запуск: сохраняет snapshot
    // Последующие: сравнивает с сохраненным
}

// Обновление snapshots после намеренных изменений:
// vendor/bin/phpunit -d --update-snapshots

Полезно для: HTML-шаблонов, JSON API ответов, сложных структур данных. Snapshots коммитятся в git.

Открыть отдельно →
13 Что такое contract testing?

Contract testing - проверка контракта между потребителем и провайдером API без прямого взаимодействия:

Потребитель описывает ожидания (contract), провайдер проверяет, что его API соответствует этим ожиданиям. Инструмент: Pact.

Подход: Consumer-Driven Contracts. Потребитель генерирует pact-файл (JSON), провайдер запускает тесты против этого контракта. Позволяет тестировать совместимость микросервисов без E2E тестов.

Открыть отдельно →
14 Как тестировать код с базой данных?
// 1. In-memory SQLite
$pdo = new PDO('sqlite::memory:');
$pdo->exec(file_get_contents('schema.sql'));

// 2. Laravel: RefreshDatabase trait
class UserTest extends TestCase {
    use RefreshDatabase;
    public function testCreate(): void {
        User::factory()->create(['name' => 'John']);
        $this->assertDatabaseHas('users', ['name' => 'John']);
    }
}

// 3. Testcontainers - реальная БД в Docker
// 4. Транзакции - откат после каждого теста
// 5. Fake repository - in-memory реализация

Рекомендация: unit-тесты с fake repository, интеграционные - с реальной БД (транзакции или RefreshDatabase).

Открыть отдельно →
15 Что такое data provider в PHPUnit?
class MathTest extends TestCase {
    #[DataProvider('additionProvider')]
    public function testAdd(int $a, int $b, int $expected): void {
        $this->assertEquals($expected, $a + $b);
    }

    public static function additionProvider(): array {
        return [
            'positive'     => [1, 2, 3],
            'negative'     => [-1, -2, -3],
            'zero'         => [0, 0, 0],
            'mixed'        => [-1, 3, 2],
        ];
    }
}

Data provider запускает тест для каждого набора данных. Имена наборов (ключи массива) отображаются в отчете. Метод должен быть static.

Открыть отдельно →
16 Что такое setUp/tearDown?
class UserServiceTest extends TestCase {
    private UserService $service;
    private PDO $db;

    // Выполняется перед КАЖДЫМ тестом
    protected function setUp(): void {
        parent::setUp();
        $this->db = new PDO('sqlite::memory:');
        $this->service = new UserService($this->db);
    }

    // Выполняется после КАЖДОГО теста
    protected function tearDown(): void {
        $this->db = null;
        parent::tearDown();
    }

    // Перед ВСЕМИ тестами класса (один раз)
    public static function setUpBeforeClass(): void {}

    // После ВСЕХ тестов класса
    public static function tearDownAfterClass(): void {}
}
Открыть отдельно →
17 Что такое assertions?
// Основные assertions PHPUnit
$this->assertEquals($expected, $actual);      // ==
$this->assertSame($expected, $actual);         // ===
$this->assertTrue($value);
$this->assertFalse($value);
$this->assertNull($value);
$this->assertNotNull($value);
$this->assertCount(3, $array);
$this->assertContains('item', $array);
$this->assertArrayHasKey('key', $array);
$this->assertInstanceOf(User::class, $obj);
$this->assertStringContainsString('needle', $haystack);
$this->assertMatchesRegularExpression('/\d+/', $str);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid email');

// Pest expectations
expect($value)->toBe(42)
    ->toBeInstanceOf(User::class)
    ->toHaveCount(3);
Открыть отдельно →
🧠Квиз 🏆Лидеры 🎯Собесед. 📖Вопросы 📚База зн.