Вопрос касается не только TDD-разработки, но и написания качественных unit-тестов в целом.
В прошлой заметке был тест на проверку отправки юзеру в бот ссылки для входа на сайт. Вот код (я еще про Argument::that() хотел отдельно написать):
/**
* @test
*/
public function user_issues_login_command_and_gets_a_message_with_login_button(): void
{
$user = User::factory()->create(['telegram_username' => 'mr_phpunit', 'telegram_id' => '123456789']);
$bot = Bot::factory()->create(['user_id' => $user->id,]);
$bot->settings()->create(['register' => TRUE,]);
$update = $this->generateMessageUpdate('mr_phpunit', '', '/login');
$webhooksService = resolve(TelegramBotWebhooksInterface::class);
$webhooksService->processUpdates($this->api->reveal(), $update);
// ожидаем что будет вызван метод sendMessage() где в тексте сообщения будет Please click the button below...
// а ссылка в кнопке Login будет содержать роут быстрого логина
$this->api->sendMessage(Argument::that(function ($params) use ($user) {
return $params['text'] === "Please click the button below to log in with your account. You will be automatically redirected to your dashboard." &&
strpos($params['reply_markup']['inline_keyboard'][0][0]['url'], route('bot.quick-login', ['user' => $user->id])) === 0;
}))->shouldHaveBeenCalled();
}
Здесь мы проверяем, что пользователю в конце ушёл в виде сообщения определённый текст. Каковой текст и является визуалом, то есть представлением. Это вывод текста и кнопки-ссылки в форме телеграм-сообщения. И этот текст тестировать не нужно.
Почему? Потому что текст (представление) не является функциональной частью нашего решения. Мы можем написать какое угодно другое сообщение, но пока в нём содержится ссылка на логин, наша фича работает корректно. То есть конкретное содержимое текста совершенно не имеет значения.
Почему ещё? Потому что в ходе развития проекта мы текст можем много раз улучшить и переписать. Что повлечёт необходимость менять соответствующие тесты. Безо всякой на то нужды, поскольку кнопку и роут входа мы не меняли.
Иными словами, при написании тестов, будь то TDD, или просто тесты из общих соображений, мы тоже должны отделять бизнес-логику от представления. Тест — это не застывшее описание реализации, а спецификация поведения программы. А смена текста сообщения (представления) никак не меняет поведение (бизнес-логику). Поэтому тест не должен ломаться при смене текста.
Текст был бы уместен, если бы кроме него никаких проверяемых результатов у нашей фичи бы не было. И то, тогда лучше проверить не наличие конкретных букв, а название строки из массива локализаций. Тогда проверку в упомянутом тесте можно было бы переписать так:
$this->api->sendMessage(Argument::that(function ($params) use ($user) {
return $params['text'] === __('login_link.description') &&
strpos($params['reply_markup']['inline_keyboard'][0][0]['url'], route('bot.quick-login', ['user' => $user->id])) === 0;
}))->shouldHaveBeenCalled();
Возвращаясь к Argument::that() — как вы уже могли заметить, этот статичный метод (из мок-библиотеки Prophecy) позволяет проверять в переданных аргументах только то, что действительно нужно проверить. А без него методы типа shouldHaveBeenCalled() будут проверять все аргументы на точное совпадение. Что сделает наши тесты очень деревянными.
Пример я взял из телеграм-бота просто потому что он был под рукой. Но абсолютно то же самое применимо и к веб-страницам. Не нужно, 10 раз не подумав, загонять в тесты конкретную верстку, или конкретные тексты. Нашим тестам от этого только вред: вывести фичу не поможет, а исправлять тесты потом придётся часто, причем безо всякой пользы для кода.
А для тестирования визуала есть более подходящие способы: инструменты, сравнивающие референсные страницы с заранее сохраненными скринами попиксельно и поднимающие шухер, если вдруг что-то куда-то съехало. Ghost Inspector, например.

