Меня не нужно убеждать, что автотесты нужны при разработке, это я уже сделал для себя сам.
Основной вопрос, как правильно написать автотест, чтобы он был в помощь, а не в обузу?
Я прочитал уже немало статей и посмотрел ряд видео об автотестах, но это все был либо чисто теоретический материал,
либо там разбирались больше "академические" примеры...
Вот собственно у меня есть конкретная задача (на самом деле упрощенный вариант реальной задачи), с которой я не могу справиться и прошу помощи.
Нужно написать функцию/класс, которая бы получала на вход некоторый текстовый параметр $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.: Я понимаю, что все Вы люди занятые, но если решите помочь, то пишите, пожалуйста, свои ответы более-менее развернуто! Не нужно писать одну фразу "Все неправильно" или "Ты безнадежен".