17 вопросов
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.
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],
];
}
}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, лучше читается.
// 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());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 - тесты менее хрупкие при рефакторинге.
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(); // в tearDownMockery поддерживает: partial mocks, spy, named mocks, expectation declarations, hamcrest matchers.
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-вызов).
TDD (Test-Driven Development) - разработка через тестирование. Цикл Red-Green-Refactor:
Цель: не 100% покрытие, а дизайн кода через тесты. TDD приводит к лучшей архитектуре: маленькие классы, четкие интерфейсы, слабая связанность.
Нет универсального числа, но ориентиры:
Важнее quality, чем quantity: покрытие бизнес-логики и edge cases важнее покрытия boilerplate. Mutation testing (Infection) - лучшая метрика качества тестов, чем code coverage.
Mutation testing - проверка качества тестов путем внесения мутаций в код:
# Infection (PHP mutation testing framework)
composer require --dev infection/infection
vendor/bin/infection --min-msi=70Infection изменяет код (меняет + на -, true на false, > на >=) и проверяет, ловят ли тесты эти изменения. Метрика MSI (Mutation Score Indicator) показывает процент "убитых" мутантов. Если мутант "выживает" - тесты недостаточно проверяют логику.
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.
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.
Contract testing - проверка контракта между потребителем и провайдером API без прямого взаимодействия:
Потребитель описывает ожидания (contract), провайдер проверяет, что его API соответствует этим ожиданиям. Инструмент: Pact.
Подход: Consumer-Driven Contracts. Потребитель генерирует pact-файл (JSON), провайдер запускает тесты против этого контракта. Позволяет тестировать совместимость микросервисов без E2E тестов.
// 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).
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.
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 {}
}// Основные 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);