Как можно протестировать запросы к базе данных и ответы от нее с помощью PHPUnit?

Здравствуйте! <br/> <br/> Я долго избегал тестирования из-за сложностей, которые испытывал при первом знакомстве с PHPUnit. Однако сейчас решил преодолеть это и разобраться в теме. <br/> <br/> Если в отношении тестирования основной логики у меня уже есть понимание, полученное из документации к PHPUnit, то я не могу разобраться с тестированием запросов к базе данных. <br/> <br/> После поиска информации я наткнулся на несколько подходов: <br/> 1. Создать тестовую базу данных с фиктивными данными и использовать метод <code>setUp()</code> для выполнения полноценного запроса через PDO (я услышал, что этот метод применяют в Laravel и Symfony). <br/> 2. Использовать Mock, содержащий массив, который симулирует ответ из базы данных. Именно с этим методом я испытываю трудности и не понимаю, как его правильно реализовать. <br/> <br/> При поисках более детальной информации я наталкиваюсь на статьи, либо очень простые, либо рассчитанные на опытных пользователей. <br/> <br/> Можете, пожалуйста, помочь мне прояснить следующие моменты? <br/> <br/> Заранее благодарю!
  • 2 ноября 2024 г. 20:22
Ответы на вопрос 2
Здравствуйте!

Отлично, что вы решили заниматься тестированием! Это важная часть разработки, и использование PHPUnit для тестирования запросов к базе данных — это правильный шаг. Давайте рассмотрим оба подхода, которые вы упомянули, и немного углубимся в детали.

### 1. Тестовая база данных

Если вы хотите протестировать свою функциональность с реальными запросами к базе данных, наиболее распространенный подход — это создание тестовой базы данных и использование `setUp()` метода в тестах.

Вот пример, как вы можете это реализовать:

```php
use PHPUnit\Framework\TestCase;
use PDO;

class YourDatabaseTest extends TestCase
{
    protected $pdo;

    protected function setUp(): void
    {
        // Подключение к тестовой базе данных
        $this->pdo = new PDO('mysql:host=localhost;dbname=testdb', 'username', 'password');
        $this->createSchema(); // Создайте схему для тестов
        $this->populateData(); // Заполните тестовыми данными
    }

    protected function tearDown(): void
    {
        // Очистка после тестов
        $this->pdo = null; // Закрытие соединения
    }

    protected function createSchema()
    {
        // Создайте таблицы для тестирования
        $this->pdo->exec("CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), email VARCHAR(100))");
    }

    protected function populateData()
    {
        // Заполнение тестовыми данными
        $this->pdo->exec("INSERT INTO users (name, email) VALUES ('John Doe', '[email protected]')");
    }

    public function testUserCount()
    {
        $stmt = $this->pdo->query("SELECT COUNT(*) FROM users");
        $count = $stmt->fetchColumn();

        $this->assertEquals(1, $count);
    }
}
```

Здесь вы подключаетесь к тестовой базе данных, создаете схему и заполняете ее данными. Каждый тест будет работать с этой тестовой базой данных.

### 2. Использование Mock-объектов

Если вы хотите изолировать вашу логику, можно использовать Mock-объекты для имитации взаимодействия с базой данных. Это полезно, когда вы хотите протестировать логику без обращения к реальной базе данных.

Вот пример, как вы можете использовать Mock в PHPUnit:

```php
use PHPUnit\Framework\TestCase;

class UserRepository
{
    protected $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function getUserCount()
    {
        $stmt = $this->pdo->query("SELECT COUNT(*) FROM users");
        return $stmt->fetchColumn();
    }
}

class UserRepositoryTest extends TestCase
{
    public function testGetUserCount()
    {
        // Создаем mock для PDO
        $pdoMock = $this->createMock(PDO::class);
        $stmtMock = $this->createMock(PDOStatement::class);

        // Настройка поведения метода query
        $pdoMock->method('query')->willReturn($stmtMock);
        $stmtMock->method('fetchColumn')->willReturn(5);

        // Тестируем UserRepository
        $repository = new UserRepository($pdoMock);
        $count = $repository->getUserCount();

        $this->assertEquals(5, $count);
    }
}
```

В этом примере мы создаем Mock-объекты для `PDO` и `PDOStatement`, чтобы изолировать тестируемую логику. Мы настраиваем ожидаемое поведение mock-объектов, чтобы они возвращали нужные данные без обращения к базе данных.

### Итог

1. **Тестовая база данных** — подход, который позволяет тестировать запросы в реальных условиях. Это классыический способ тестирования.
2. **Mock-объекты** — это подход, который позволяет тестировать отдельные части вашего кода, не обращаясь к базе данных. Это отлично подходит для юнит-тестов.

Какой из подходов использовать, зависит от ваших требований и стажа работы с тестированием. Начните с тестовой базы данных, чтобы понять основы, а затем попробуйте Mock-объекты для более изолированных тестов.

Если у вас есть дополнительные вопросы или что-то не ясно, не стесняйтесь спрашивать! Удачи в тестировании!
Мне кажется, что найденные варианты работают не так. Используются они оба, а какой именно - зависит от того, что именно вы тестируете - запрос или ответ. 

Если говорить про "запросы к БД", то для таких тестов "массив с данными" использовать просто глупо. Это получится какой-то формальный тест: "проверяем, что метод возвращает массив из трех строк - и тут же возвращаем этот самый массив". В чем смысл? Если вы тестируете запрос к БД, то и надо тестировать запрос к БД. По-другому никак.

Здесь я отвлекусь, и порассуждаю на тему того, что на самом деле тестирование - это гораздо более трудоёмкая задача, чем обычно считается. И как следствие, большая часть тестов - это такая вот туфта. Либо тест заранее возвращает нужные данные, либо тестирует один-два кейса. А вариантов неправильных входящих данных ведь может быть огромное количество. То есть по-хорошему на такой заведомо сложный (и принципиально неделимый!) метод нужно десятка два тестов.

И сюда же использование для тестов БД другой системы. Например основная БД MySQL, а для тестов используется Sqlite. Тут сразу можно сказать, что это профанация. Различие даже в какой-то одной настройке БД может повлиять на результаты запроса (и теста как следствие) - а тут и вовсе используется совсем другая БД.

С другой стороны, работу с БД скорее стоит тестировать не в юнит тестах, а скажем в интеграционных. Но не будем углубляться.


Массив же "с данными, симулирующими ответ из базы данных" используется на следующему уровне, там где требуется "ответ из базы данных". Взьмём метод, который использует данные, полученные из БД. Например авторизация юзера. Этот метод должен не сам лезть в базу, а дёргать отдельный метод, вполне возможно, что совсем другого класса. И вот чтобы протестировать авторизацию, вы и мокаете метод для работы с БД, и из этого мока возвращаете тот самый массив без всякого обращения к бд.

Здесь нелишне будет напомнить название доклада с одной из последних конференций по пхп. Дословно не помню, но что-то вроде "Делайте методы как можно короче. Ваши тесты скажут вам спасибо." Собственно, именно при тестировании очень быстро начинаешь понимать удобство атомарных (то есть выполняющих какую-то одну простую операцию) методов. А без тестов пройдёт довольно много времени, пока не понадобится этот длинный метод отрефакторить, и по итогам менять кучу кода, который его вызывает.

Так что разбивайте ваши длинные методы на мелкие, и тогда вопрос, как их тестировать, в большинстве случаев отпадёт сам собой.
Похожие вопросы