Как протестировать запрос к базе данных?

«Как протестировать запрос к базе данных?» — вопрос из категории Тестирование, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Тестирование запросов к БД можно организовать несколькими способами, в зависимости от уровня изоляции и целей теста.

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

// Пример с PHPUnit
$repositoryMock = $this->createMock(UserRepository::class);
$repositoryMock->expects($this->once())
    ->method('findActiveUsers')
    ->willReturn([
        new User('John'),
        new User('Jane')
    ]);

$service = new UserService($repositoryMock);
$result = $service->getActiveUsers();
$this->assertCount(2, $result);

2. Использование тестовой БД (Интеграционные тесты) Тесты выполняются против реальной, но изолированной базы данных (часто SQLite в памяти или Docker-контейнер). Ключевой момент — управление состоянием.

protected function setUp(): void
{
    parent::setUp();
    // Подключение к SQLite в памяти
    $this->pdo = new PDO('sqlite::memory:');
    $this->pdo->exec('CREATE TABLE users (id INTEGER, name TEXT)');
    // Загрузка фикстур
    $this->loadFixtures();
}

public function testFindUserById()
{
    $repository = new UserRepository($this->pdo);
    $user = $repository->find(1);
    $this->assertEquals('John', $user->name);
}

protected function tearDown(): void
{
    $this->pdo = null;
    parent::tearDown();
}

3. Транзакционное тестирование Позволяет использовать ту же схему БД, что и в продакшене, с откатом изменений после каждого теста.

use PHPUnitFrameworkTestCase;
use DoctrineDBALConnection;

class TransactionalTest extends TestCase
{
    private Connection $connection;

    protected function setUp(): void
    {
        $this->connection = TestConnectionFactory::create();
        $this->connection->beginTransaction(); // Начало транзакции
    }

    public function testComplexQuery()
    {
        $repository = new UserRepository($this->connection);
        $repository->insert(new User('Test'));
        // Тест выполняется внутри транзакции
        $users = $repository->findAll();
        $this->assertNotEmpty($users);
    }

    protected function tearDown(): void
    {
        $this->connection->rollBack(); // Откат всех изменений теста
        parent::tearDown();
    }
}

Основные практики:

  • Изоляция: Каждый тест должен стартовать с предсказуемого состояния БД (фикстуры, транзакции).
  • Скорость: Используйте in-memory БД для быстрых юнит-тестов.
  • Проверка данных: Утверждайте не только факт выполнения запроса, но и корректность возвращённых данных.
  • Тестирование ошибок: Обязательно тестируйте граничные случаи и обработку ошибок БД (например, дублирование ключа).