В первой части мы рассмотрели примеры тестов, из которых не все одинаково полезны. Затем попытались определиться, что же такое качество ПО, и предложили "распрямлять" код и выводить программы из требований. Рассмотрели классификацию ошибок.


Видя необоснованные надежды, возлагаемые на юнит-тесты, хотелось бы понять, что в действительности можно ожидать от тестов.





Удобный запуск разрабатываемого модуля


При разработке модуля приходится его часто запускать с одними и теми же аргументами. Если подготовку аргументов и запуск модуля оформить в виде юнит-теста, то частые запуски не будут отнимать много времени. Можно написать несколько юнит-тестов, чтобы проверять работу модуля в интересующих режимах.


Когда разработка модуля завершена, необходимость в таких тестах в целом отпадает. Однако, в последующем, при расширении функционала модуля, те же тесты легче доработать и использовать для запуска изменённого модуля, чем писать заново. Такие тесты можно пометить, прямо указав в их названии run, debug ...


Воспроизведение известных багов ("регрессионное тестирование")


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


Нужен ли тест после исправления ошибки? Иногда ошибка может "возвращаться". В частности, если над проектом работают несколько человек, то другой разработчик, не знающий причин внесения изменений, может изменить код так, что та же ошибка проявится снова. Также код, где уже была обнаружена ошибка, с бо́льшей вероятностью содержит и другие ошибки. Адаптировать имеющийся тест для воспроизведения новой ошибки может быть легче, чем писать тест с нуля.


Если ошибка была связана с несовместимостью с новой версией библиотеки, то сохранение теста позволит предотвратить случайное обновление.


В названии такого теста логично использовать ссылку на воспроизводимый баг — bug1234, regress4321, ...


Одиночный тест "прямолинейного" кода


Если код, работоспособностью которого мы интересуемся, является "прямолинейным", то для его проверки достаточно выполнить однократный запуск. Исходя из свойств "прямолинейности", код должен работать и для других значений.


В частности, если у нас имеется код с дженериками, то он, обычно, является прямолинейным относительно типа. А значит, для проверки работоспособности такого кода может быть достаточно одного теста при каком-то значении типа.


Smoke-тест ("Дымовой" тест)


Такой тест нужен для запуска и проверки в простейшем случае. Позволяет убедиться, что программа вообще хоть в каких-то условиях хоть как-то работает. Подходящее название — smokeTest, runAll, ...


Набор тестов-индикаторов на каждое требование


Разновидностью Smoke-тестов можно считать набор тестов-индикаторов по одному на каждую "фичу". Каждый такой тест проверяет наличие определённой функциональности и падает в случае её отсутствия. Такой набор позволяет не пытаться объять необъятное и доказать соответствие требованиям, а решить вполне достижимую задачу — что для каждого задокументированного требования предусмотрен/существует код и при некоторых входных данных требование выполняется (в качестве документации может использоваться номер issue или ссылка на дизайн). В случае, если разработчик случайно произведёт изменение, отключающее ранее реализованную функциональность, соответствующий тест-индикатор позволит это оперативно обнаружить. Имеет смысл в названии таких тестов прямо указывать их индикаторную роль (featureMSP3142, fixMSP4231, indicatorUserAuthFeature, ...).


Юнит-тесты как упражнение


"Ногти и волосы даны человеку для того, чтобы доставить ему постоянное, но легкое занятие." © Козьма Прутков


Если в компании имеется корпоративная политика по написанию юнит-тестов, безотносительно их пользы для проекта, то такая политика служит постоянным источником несложной работы, которая может использоваться разработчиками для отдыха и разминки, своего рода упражнения для ума.


Если при этом допускается и подразумевается выполнение рефакторинга (изменения cигнатур функций, структуры классов и т.д.), то программный интерфейс может стать более удобным для вызова программистом, тем самым, улучшить будущие примения этого кода. Учлучшение происходит за счёт того, что разработчик думает не только о реализации, но и о том, как пользоваться разрабатываемым модулем. Подходящее название для таких тестов — test..., excercise..., coverage..., ...


Документирование API


При написании документации полезно приводить примеры вызовов API и показывать с помощью assert'ов, что ожидается в результате. Такие примеры из документации могут быть оформлены в виде юнит-тестов и будут поддерживаться в актуальном состоянии в ходе эволюции модуля (так как все примеры часто перекомпилируются и выполняются). Такие тесты могут иметь имя example..., doc..., apiTest....


Следует однако иметь в виду, что только юнит-тесты не могут служить единственной документацией. Зачастую в библиотеке или модуле имеются некоторые высокоуровневые идеи, которые обычному разработчику не удаётся быстро уловить в ходе "реверс-инжиниринга" кода или тестов. Такие идеи лучше всего всё-таки сообщать посредством текста и других средств коммуникации между людьми. Например, при реализации монады тест может проверять законы, которым подчиняются все монады. Но такого теста, по-видимому, недостаточно, чтобы разработчик понял и смог использовать такую библиотеку, если раньше не встречался с новыми понятиями.


По-видимому, не стоит также злоупотреблять желанием всё "задокументировать" в виде тестов. Если имеется десяток однотипных модулей, то, на мой взгляд, нет необходимости документировать каждый одинаково подробно. Достаточно задокументировать один, а для остальных указать отличия.


Борьба с глобальными переменными


Глобальные изменяемые переменные представляют известную проблему в приложении. При тестировании, когда требуется создавать неполные конфигурации системы и многократно их запускать в предсказуемых условиях, эта проблема встаёт в полный рост. Особенно при попытках распараллелить выполнение тестов. Помучившись какое-то время, наблюдательный разработчик может прийти к выводу о необходимости избавления от глобальных переменных. Тем самым, написание тестов может подтолкнуть к оздоровлению кодовой базы.


Чего не надо ожидать от юнит-тестирования


  1. "Если все юнит-тесты выполняются, то программа в целом соответствует требованиям."
  2. "Если покрытие строк кода выше 95%, значит, ..."
    • "… вероятность серьёзной проблемы в программе не выше 5%."
    • "… при реализации следующей функции вероятность возникновения серьёзной проблемы в программе не выше 5%."
    • "… внесение небольшого изменения не приведёт к катастрофическим последствиям с вероятностью 95%."
    • "… требования выполняются на 95%."
    • "...(любое утверждение о требованиях)."
  3. "Если покрытие строк кода ниже 50%, значит, ..."
    • "… в программе много багов, особенно в строчках, не покрытых тестами."
    • "… программа вряд ли соответствует требованиям."

С учётом принципиальной ограниченности табличных функций, такие ожидания не имеют под собой оснований.


Какие тесты полезны?


Само по себе падение теста ещё не означает, что причину ошибки удастся легко определить. После обнаружения упавшего теста следует процесс troubleshooting'а (локализации и устранения проблемы). При этом тест может как помогать в этом процессе, так и затруднять его.


Например, если в тесте имеется утверждение assert count == 10, проверяющее количество элементов, то в случае ошибки будет трудно разобраться, что же именно ожидалось. Если вместо этого перечислить сами элементы assert actualList == expectedList, то, сравнивая два списка, можно будет установить, какого именно элемента не хватает. Такая информация очень поможет при поиске ошибки. Главный вопрос, который должен задавать себе разработчик теста, — "достаточно ли будет информации при падении теста, чтобы понять, что делать дальше".


Заключение. Ограниченная польза юнит-тестов


По-видимому, юнит-тесты не улучшают качество программного обеспечения напрямую. Юнит-тесты в первую очередь полезны самим разработчикам, а также несколько улучшают процесс разработки в целом, что косвенно влияет на качество ПО:


  • улучшение модульности программы,
  • переключение фокуса разработчика на удобство использования разрабатываемого модуля,
  • поддержка документации в постоянно актуальном состоянии,
  • и т.п.

Пользу юнит-тестов следует соотносить с затратами на их разработку, поддержку, модификацию кода для удобства тестирования, и рассматривать альтернативные механизмы улучшения качества. Если разработка юнит-тестов требует больших усилий, то, возможно, овчинка выделки не стоит. Сами по себе тесты серебряной пулей не являются и не обеспечивают высокого качества ПО автоматически. А затраты на их разработку, поддержку, рефакторинг — вполне себе заметны.


Вся серия заметок:


  1. Примеры тестов.
  2. Что делать?
    1. Качество ПО.
    2. "Прямолинейность" кода.
    3. Классификация ошибок.
    4. Эквивалентность функций.
    5. Применимость тестов.
  3. Исправление примеров.

Комментарии (0)