Квест → Как хакнуть форму
Прошли: 77
На днях я наткнулся на следующий код в проекте:
class Users { public function __construct(PDO $pdo) { $this->pdo = $pdo; } public function getAllUsers() { $stmt = $this->pdo->prepare('SELECT * FROM users'); return $stmt->fetchAll(); } }
И был вот такой тест для проверки этого кода:
class UserTest extends TestCase { public function testGetAllUsers() { $pdo = m::mock(PDO::class); $stmt = m::mock(PDOStatement::class); $pdo->shouldReceive(‘prepare’)->andReturn($stmt); $pdoStmt->shouldReceive(‘fetchAll’)->andReturn($userArray); $users = new Users($pdo); $result = $users->getAllUsers(); $this->assertEquals($userArray, $users); } }
Обратите внимание, что я опустил остальную часть класса User, а также массив пользователей, который возвращается в тесте.
Этот тест на самом деле даёт нам 100%-ое покрытие кода в методе getAllUsers()
. Но, к сожалению, для любой практической цели, этот тест полностью бесполезен.
Этот unit-тест бесполезен, потому что вместо того, чтобы тестировать внутреннюю работу метода getAllUsers()
, он на самом деле тестирует Mockery. С первых строк теста, мы создаем моки для PDO и PDOStatement, и мы передаем эти моки в класс Users
. Что мы на самом деле тестируем -- так это то, что Mockery правильно возвращает массив; мы вообще не трогаем базу.
Если бы getAllUsers()
действительно делал что-то, например, обрабатывал пользователей каким-то образом, тогда было бы разумно создать моки и соответствующим образом тестировать для различных условий алгоритма. Но здесь не тот случай.
Слишком часто, в поиске 100%-го покрытия кода unit-тестами, я вижу такие тесты, как этот. Они не служат практической цели, кроме достижения цели тестового покрытия. И что ещё хуже, они на самом деле не улучшают качество приложения.
Вместо того, чтобы писать модульный(unit) тест, здесь лучше написать интеграционный тест или функциональный тест. Эти тесты требуют от нас, чтобы мы напрямую взаимодействовали с базой данных, но предоставляют гораздо более ценную информацию о работоспособности и состоянии нашего приложения. Бесполезный unit-тест предоставляет очень нам мало, если не сказать никакой пользы. Полезный функциональный тест дает нам огромные преимущества.
Написание функциональных тестов является сложной задачей и требует мастерства и решимости. И избегая такие бесполезные юнит-тесты, вы убедитесь, что тесты, которые будут написаны, будут полезны и выгодны в будущем.
От переводчика:
Первый же комментарий к оригинальному посту гласит:
Я не согласен с этим постом. Этот тест очень прост как и сам тестируемый код, и просто проверяет, что взаимодействие между PDO и Users выполнено правильно.
Это ни в коем случае не означает, что нужно проверять работает ли PDO, поскольку нам не нужно тестировать это, т.к. по определению PDO должен быть сам протестирован. Это просто проверка того, что Users правильно используют PDO.
Однако в нём упущены некоторые вещи, такие как проверка, что prepare() должен быть вызыван только один раз и при этом как параметр передан правильный запрос.
Есть и положительный комментарий:
Я согласен с автором в этом конкретном случае.
Это метод репозитория, и его единственной целью должно быть выполнение запроса. Как еще мы будем уверены, что запрос возвращает ожидаемый результат, если мы не протестируем его с через БД?
Для этого нам нужен интеграционный тест. И если мы уже протестируем его с помощью теста интеграции, то unit-тест будет избыточным.
Как вы считаете стоит ли писать такие тесты? И как на самом деле вы обычно поступаете?
// считать и поступать(реализовывать) -- разные вещи :)
Бесполезный тест получается. Лучше использовать SQLite для тестового окружения, быстро разворачивать дамп с фикстурами. А уже потом проверять что возвращает результат обращения к бд. Тут идет select *, а значит нужно проверить список возвращаемых столбцов из бд и проверить на возникновение разных ошибок.