> Пишете ли вы unit и интеграционные тесты (PHP)

Уровень: senior · Роль: backend · Язык: PHP · Категория: Технические вопросы

Компании: Travelata

Стек: PHP, C# /.NET

> Пример ответа

Короткий ответ

Да, пишу и unit, и интеграционные тесты, но с разным приоритетом. Unit-тесты покрывают изолированную бизнес-логику и чистые функции, интеграционные - взаимодействие с внешними зависимостями: БД, Redis, API, файловой системой. В PHP-проектах чаще использую PHPUnit и Pest, в C# - xUnit и NUnit. Стараюсь придерживаться принципа тестовой пирамиды: много быстрых unit-тестов, меньше интеграционных, ещё меньше end-to-end.

Подробное объяснение

Unit-тесты проверяют один класс или метод в изоляции, с моками всех зависимостей. Они быстрые, стабильные и помогают быстро находить регрессии при рефакторинге. Интеграционные тесты проверяют совместную работу нескольких компонентов: например, репозиторий с реальной БД, сервис с кэшем, контроллер с middleware.

Для PHP типичный стек: PHPUnit, Mockery для моков, Testbench для Laravel-пакетов, RefreshDatabase для тестов с БД. Для C#: xUnit, Moq или NSubstitute, WebApplicationFactory для интеграционных тестов ASP.NET Core, Testcontainers для Docker-зависимостей.

Ключевой trade-off: скорость против достоверности. Unit-тесты почти мгновенные, но не гарантируют, что код работает с реальными зависимостями. Интеграционные тесты медленнее и требуют окружения, но дают уверенность в корректности связки.

На практике покрываю unit-тестами доменную логику, валидаторы, калькуляторы, сервисы без I/O. Интеграционными - репозитории, миграции, API-эндпоинты, очереди, внешние вызовы через контрактные тесты.

На практике

В реальных проектах соотношение примерно 70/30 в пользу unit-тестов. Для интеграционных тестов использую отдельную тестовую БД, обычно SQLite в памяти для простых случаев или PostgreSQL в Docker для продакшен-совместимости. В C# часто применяю Testcontainers, чтобы поднимать реальный SQL Server или Redis.

Важно, чтобы интеграционные тесты были идемпотентными: каждый тест очищает данные после себя или использует транзакции с откатом. В PHP с Laravel это решается через RefreshDatabase, в C# - через транзакционные фикстуры или очистку таблиц в setup.

Также практикую тесты на миграции: проверяю, что схема БД создаётся и откатывается корректно. Это часто упускают, но именно здесь ломается интеграция с продакшен-окружением.

Пример кода

Пример интеграционного теста на PHP с PHPUnit и Laravel:

PHP
public function test_order_total_is_calculated_with_discount(): void
{
$user = User::factory()->create();
$product = Product::factory()->create(['price' => 1000]);
$order = $this->actingAs($user)
->postJson('/api/orders', [
'items' => [['product_id' => $product->id, 'quantity' => 2]],
'promo_code' => 'SAVE10',
])
->assertOk()
->json('data');
$this->assertDatabaseHas('orders', [
'id' => $order['id'],
'total' => 1800,
]);
}

Пример unit-теста на C# с xUnit и Moq:

CSHARP
[Fact]
public void CalculateTotal_AppliesDiscount_WhenThresholdExceeded()
{
var discountService = new Mock<IDiscountService>();
discountService.Setup(x => x.GetDiscount(It.IsAny<decimal>()))
.Returns(0.1m);
var calculator = new OrderCalculator(discountService.Object);
var total = calculator.CalculateTotal(1000m);
Assert.Equal(900m, total);
}

Как отвечать на собеседовании

Начните с прямого ответа "да", затем кратко опишите свой подход. Упомяните, что выбор типа теста зависит от того, что именно проверяется: изолированная логика - unit, связка с инфраструктурой - интеграционные. Расскажите про конкретный стек и инструменты, которыми пользуетесь.

Приведите пример из практики: как интеграционный тест поймал реальный баг, который unit-тесты пропустили, например, несоответствие типов в миграции или неправильный порядок сортировки при выборке из БД. Это покажет, что вы понимаете ценность обоих уровней.

Если спросят про покрытие, не называйте абстрактные проценты. Лучше сказать: "Покрываю критичные бизнес-сценарии и сложную логику, а не гонюсь за метрикой". Упомяните, что используете coverage только как сигнал, а не как цель.

Что проверяет интервьюер

Интервьюер оценивает:

  • понимание разницы между unit и интеграционными тестами, их сильных и слабых сторон;
  • умение выбирать правильный уровень тестирования для конкретного случая;
  • знание инструментов и паттернов: моки, фабрики, транзакционные тесты, testcontainers;
  • практический опыт: как вы организуете тестовое окружение, как боретесь с нестабильностью тестов;
  • отношение к тестированию в целом: считаете ли вы это обязательной частью разработки или формальностью.

Также смотрят, насколько вы умеете объяснять trade-off между скоростью и достоверностью, и как выстраиваете стратегию тестирования в проекте.

Типичные ошибки

  • Говорить "да, пишу тесты", но не уметь объяснить, что именно покрываете и почему.
  • Путать unit и интеграционные тесты, называя тесты с БД unit-тестами.
  • Использовать моки для всего подряд, включая простые DTO и value objects - это усложняет тесты без пользы.
  • Писать интеграционные тесты, которые зависят от внешних сервисов вроде реального API или продакшен-БД - они нестабильны и ломаются без причины.
  • Игнорировать тесты на миграции и схемы БД - это частая точка отказа в интеграции.
  • Гнаться за 100% покрытием, забывая про качество сценариев.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью