Comments 7
На код же невозможно смотреть, где подсветка синтаксиса?
Вообще говоря, использование классических моков это уже не модульные тесты, а интеграционные. И основное их назначение не изоляция от внешних источников данных (это задача стабов, хотя в PHPUnit такой отдельной сущности нет — её заменяет мок, возвращающий результаты), а контроль взаимодействия подсистем.
А в одном тесте, вернее в одном методе мок-объекта, совмещать функциональность моков и стабов не очень хорошо, по-моему. Или мы тестируем, что наш метод вызывает другие с определенными параметрами, в определенном порядке и т. п., или что он корректно обрабатывает получаемый извне результат. Большой соблазн это объединить, но именно так мы получаем хрупкие и нечитаемые тесты, которые тестируют кучу вещей одновременно.
Большой соблазн написать что-то вроде: «тест проверяет, что в методе первой вызывается ровно один раз $db->query со строковым параметром „SELECT * FROM users“, возвращающую не false, строго после неё вызывается $db->fetch с параметром, возвращенным ранее, столько раз сколько она возвращает не false, а массив(onConsecutiveCalls(array(...), array(...), array(), false), строго после неё один раз вызывается $db->free с тем же параметром, а главное, что метод возвращает массив объектов соответствующий возврату $db->fetch ранее. Но представьте как с этим работать. Нужно хотя бы отделить проверку корректности вызовов методов $db (классические моки, все эти excepts, with, at и т.п.) — интеграционные тесты — и проверку работы собственно метода (моки в роли стабов — will), пускай и 90% кода будет пересекаться (его можно вынести в setUp или отдельный метод). Главное, чтобы сфейливший тест однозначно указывал на место ошибки одним фактом своего фейла и своим названием, без необходимости залезать в код теста и тестируемый код. Если мы говорим о модульном тестировании.
В вашем случае у теста (последний вариант) есть три потенциальных точки фейла: метод getTemperature не вызовется ровно один раз, метод showWord не вызовется ровно один раз и метод showWord, вызовется ровно один раз, но не с ожидаемым параметром. Одним тестом вы тестируете, по сути, логику взаимодействия с датчиком (что getTemperature вызывается один раз), логику взаимодействия с индикатором (что showWord вызывается один раз) и логику самого метода process (что параметр вызова showWord соответствует температуре, то есть ещё и неявное тестирование метода getWord).
Вам ничего не скажет сообщение о том, что test_process сфейлил. Более того, без изучения кода и теста, и всего класса вам ничего не скажет даже сообщение о том, что метод showWord вызвался не с тем параметром.
Я бы тестировал так:
0. тесты на getWord (4 штуки, 14,15,25 и 26) — быстрые модульные тесты, не требующие моков, стабов и прочих отражений/кодогенераций
1. проверяем, что getTemperature (мок getTemperature с произвольным will, „нулевой“ стаб showWord) вызывается один раз без параметров — ожидаемое взаимодействие с датчиком — один медленный интеграционный тест
2. проверяем, что showWord для какой-то температуры (стаб getTemperature c произвольным will, мок showWord) вызывается один раз с нужным значением — ожидаемое взаимодействие с индикатором — второй медленный интеграционный тест
3. (на любителя, имхо оверхид) проверяем, что getWord вызывается один раз с параметром соответствующим температуре (стаб getTemperature c произвольным will, мок getWord, „нулевой“ стаб showWord) — третий медленный интеграционный тест
Любой сфейливший тест покажет нам однозначно место ошибки — либо что-то не то с вызовом getTemperature, либо с вызовом showWord, либо с логикой getWord. Нет нужды много раз неявно, с использованием моков и стабов, проверять getWord, особенно учитывая, что ошибка скорее всего будет именно в нём.
А в одном тесте, вернее в одном методе мок-объекта, совмещать функциональность моков и стабов не очень хорошо, по-моему. Или мы тестируем, что наш метод вызывает другие с определенными параметрами, в определенном порядке и т. п., или что он корректно обрабатывает получаемый извне результат. Большой соблазн это объединить, но именно так мы получаем хрупкие и нечитаемые тесты, которые тестируют кучу вещей одновременно.
Большой соблазн написать что-то вроде: «тест проверяет, что в методе первой вызывается ровно один раз $db->query со строковым параметром „SELECT * FROM users“, возвращающую не false, строго после неё вызывается $db->fetch с параметром, возвращенным ранее, столько раз сколько она возвращает не false, а массив(onConsecutiveCalls(array(...), array(...), array(), false), строго после неё один раз вызывается $db->free с тем же параметром, а главное, что метод возвращает массив объектов соответствующий возврату $db->fetch ранее. Но представьте как с этим работать. Нужно хотя бы отделить проверку корректности вызовов методов $db (классические моки, все эти excepts, with, at и т.п.) — интеграционные тесты — и проверку работы собственно метода (моки в роли стабов — will), пускай и 90% кода будет пересекаться (его можно вынести в setUp или отдельный метод). Главное, чтобы сфейливший тест однозначно указывал на место ошибки одним фактом своего фейла и своим названием, без необходимости залезать в код теста и тестируемый код. Если мы говорим о модульном тестировании.
В вашем случае у теста (последний вариант) есть три потенциальных точки фейла: метод getTemperature не вызовется ровно один раз, метод showWord не вызовется ровно один раз и метод showWord, вызовется ровно один раз, но не с ожидаемым параметром. Одним тестом вы тестируете, по сути, логику взаимодействия с датчиком (что getTemperature вызывается один раз), логику взаимодействия с индикатором (что showWord вызывается один раз) и логику самого метода process (что параметр вызова showWord соответствует температуре, то есть ещё и неявное тестирование метода getWord).
Вам ничего не скажет сообщение о том, что test_process сфейлил. Более того, без изучения кода и теста, и всего класса вам ничего не скажет даже сообщение о том, что метод showWord вызвался не с тем параметром.
Я бы тестировал так:
0. тесты на getWord (4 штуки, 14,15,25 и 26) — быстрые модульные тесты, не требующие моков, стабов и прочих отражений/кодогенераций
1. проверяем, что getTemperature (мок getTemperature с произвольным will, „нулевой“ стаб showWord) вызывается один раз без параметров — ожидаемое взаимодействие с датчиком — один медленный интеграционный тест
2. проверяем, что showWord для какой-то температуры (стаб getTemperature c произвольным will, мок showWord) вызывается один раз с нужным значением — ожидаемое взаимодействие с индикатором — второй медленный интеграционный тест
3. (на любителя, имхо оверхид) проверяем, что getWord вызывается один раз с параметром соответствующим температуре (стаб getTemperature c произвольным will, мок getWord, „нулевой“ стаб showWord) — третий медленный интеграционный тест
Любой сфейливший тест покажет нам однозначно место ошибки — либо что-то не то с вызовом getTemperature, либо с вызовом showWord, либо с логикой getWord. Нет нужды много раз неявно, с использованием моков и стабов, проверять getWord, особенно учитывая, что ошибка скорее всего будет именно в нём.
Полностью согласен с вашими суждениями. Целью моей статьи является демонстрация приемов применения Mock объектов в PHPUnit. Поэтому и был выбран пример, в котором можно было бы полностью отразить все возможности использования таких объектов в одном тесте.
Проблема в том, что неудачные примеры имеют свойство «размножаться», причём, субъективно, быстрее чем удачные. Такое ощущение, что антипаттерн в коде запоминается в мозгу как паттерн даже если большими буквами написано «НИКОГДА ТАК НЕ ДЕЛАЙТЕ».
Хоть статья и не новая, но полезная — спасибо.
Sign up to leave a comment.
PHPUnit: Mock объекты