@HiDiv
fullstack разработчик (php, js, html, css, vuejs)

Как правильно покрыть тестами приведенный в примере код?

Меня не нужно убеждать, что автотесты нужны при разработке, это я уже сделал для себя сам.
Основной вопрос, как правильно написать автотест, чтобы он был в помощь, а не в обузу?
Я прочитал уже немало статей и посмотрел ряд видео об автотестах, но это все был либо чисто теоретический материал,
либо там разбирались больше "академические" примеры...

Вот собственно у меня есть конкретная задача (на самом деле упрощенный вариант реальной задачи), с которой я не могу справиться и прошу помощи.

Нужно написать функцию/класс, которая бы получала на вход некоторый текстовый параметр $paramX (в реальной задаче из несколько) и проверяла в таблице БД наличие записей удовлетворяющих условию, которое зависит от $paramX.
Если хотя бы одна такая запись есть, то возвратить True, иначе (если нет ни одной) False.

Вот пример моей реализации.

Код реализации

class GetSomeValue
{
    protected $paramX;
    protected $db;
    protected $timedate;

    public function __construct(string $paramX)
    {
        $this->paramX = $paramX;

        // Инстанс от фреймворка для работы с БД
        $this->db = DBManager::getInstance();
        // Инстанс от фреймворка для работы с датой и временем
        $this->timedate = TimeDate::getInstance();
    }

    public function getValue(): bool
    {
        $row = $this->getDbData($this->getCompleteSql());
        return !empty($row);
    }

    /**
     * Вынес в отдельную функцию и реализовал такую передачу параметров, чтобы можно было замокать
     * @param string $sql
     * @return array|false Если данные не найдены или ошибка выполнения запроса, то false, иначе массив с данными одной записи
     */
    protected function getDbData(string $sql)
    {
        return $this->db->fetchOne($sql);
    }

    protected function getCompleteSql(): string
    {
        return strtr($this->getSql(), $this->getSqlParams());
    }

    protected function getSql(): string
    {
        return "
            SELECT
                'x'
            FROM
                table_1
            WHERE
                field1 = 'CONST1' AND
                field2 = <paramX> AND
                date_open <= <curDate> AND
                date_close IS NULL
        ";
    }

    protected function getSqlParams(): array
    {
        $params = [
            '<paramX>' => $this->paramX,
            '<curDate>' => $this->getCurDate(),
        ];
        return array_map([$this->db, 'quoted'], $params);
    }

    /**
     * Вынес в отдельную функцию, чтобы можно было замокать
     * @return string Дата в формате БД yyyy-mm-dd
     */
    protected function getCurDate(): string
    {
        return $this->timedate->getNowDbDate();
    }
}



Насколько я понял, модульный тест должен тестировать строго одну функцию/метод (или даже его часть) без взаимодействия с другими сущностями. Если это так, то тогда единственное, что можно проверить, это getValue.
Замокав getDbData и getCompleteSql можно возвращать либо массив, либо false, и проверять, что будет на выходе.
У меня конечно нет особого опыта в автотестах, но на мой взгляд не особо "информативный" тест, если рассматривать всю задачу в целом.

Нужно ли тестировать отдельно защищенные методы, я так и не понял. Если все же нужно, то наверное в каждом тесте придется порождать новый класс, в котором повышать видимость метода до public и уже потом тестировать. Правда я тогда не очень понимаю, что даст тест, например, getSql? Просто проверить, что вернулась та же самая константа? Ведь проверить на синтаксическую или "логическую" правильность sql-запрос тут не получится...

С другой стороны, можно замокать только getCurDate и getDbData и тогда проверять итоговый sql-запрос (правда только визуально) и реакцию на псевдо-ответ из БД. Правда это уже получается (IMHO) не модульный, а скорее интеграционный тест, но тут я вижу явный профит. Тем более, что будут проверены почти все составляющие класса, за исключением замоканных. Однако, минус в том, что поломаться тест тоже может в любом месте и опять же мы никак не проверяем синтаксическую или "логическую" правильность sql-запроса...

И, наконец, можно для каждого теста заполнять тестовыми данными таблицу БД, либо сделать это один раз и сохранить дамп для последующей развертки. Тогда мы можем проверить все. Правда, чтобы действительно проверить все граничные варианты (перебрать их) придется выполнить много тестов. Может быть, например, так:

  • нет данных в таблице вообще.
  • есть данные, но не совпадающие со "статическими условиями" (константой или paramX).
  • если одна запись совпадающая с "динамическими условиями" (даты).
  • есть несколько записей совпадающих с "динамическими условиями" (даты).


Возможно этот список можно сократить или расширить, но перебирать придется все комбинации, а их будет на мало...

До недавнего времени, я всегда при написании тестов шел по последнему варианту. Получалось хорошее покрытие,
но количество тестов растет, как грибы после дождя. Минус, при внесении более-менее существенных изменений в реализацию, тесты ломаются пачками. Правда и чинятся они тоже пачками, но мне кажется это не совсем правильно. Чувствую, что делаю что-то не так!

Обращаюсь к вам Гуру автотестов! Подскажите, в чем я не прав и покажите на данном примере, что я делаю неправильно!

Собственно вопрос первый. Правильна ли сама реализация задачи с точки зрения ее дальнейшего покрытия автотестами? Если Нет, то что именно и как нужно поменять (желательно с примером),

Вопрос второй. Как правильно покрыть приведенный (или доработанный) код автотестами? Тоже очень хотелось бы примеров и хотя бы минимальных пояснений к ним.

Заранее спасибо за любую помощь!

P.S.: Я понимаю, что все Вы люди занятые, но если решите помочь, то пишите, пожалуйста, свои ответы более-менее развернуто! Не нужно писать одну фразу "Все неправильно" или "Ты безнадежен".
  • Вопрос задан
  • 201 просмотр
Пригласить эксперта
Ответы на вопрос 2
index0h
@index0h
PHP, Golang. https://github.com/index0h
https://github.com/index0h/php-conventions#7-тести...

Конкретно по коду:
1. Не стоит упарываться по разбиванию всего на отдельные методы. Фактическую сложность вы не уменьшите, вместо этого заставите инженера, который будет это смотреть бегать по классу что бы связать как-то ваши однострочники. Конкретно в вашем случае стоит сделать всего один метод getValue, а то что вы разбрасали по защищенным - запихнуть в getValue.

2. Инстансы db и timedate стоит передавать в конструктор явно, а от статики отказываться, на сколько это возможно.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы