Comments 39
Позиция должна быть в том, что любая ошибка, это не просто провал в коде, это также провал в защите тестирования.
Непонятная фраза. Тесты в контексте TDD/BDD — это средство фиксации поведения и детектирования его изменения. Ошибка означает, что или не зафиксировали требуемое поведение, или зафиксировали ошибочное.
По практике основная причин этого — плохо сформулированные требования или плохая работа с ними: или поведение в каком-то особом случае не определено и разработчик интерпретировал его по другому чем ожидал заказчик ("это же очевидно, что бухгалтер не может подписывать счёт раньше менеджера"), или определено ошибочно, а разработчик просто реализовал его, или никто, прежде всего команда QA не заметили, что опеределенное четко сформулированное требование не выполняется. Чаще всего первая или вторая ситуация, то есть плохие требования.
Насколько я понял, тестирование — это верификация в программировании. Во всяком случае, юниты тестирование.
А как обстоят дела с моделированием? Какие комплексные технологии и методологии используются при моделирование работы кода? Возможно, следует написать об этом отдельную статью?
Если бы мне пришлось проектировать какую нибудь архитектуру, пусть даже простую 3 ступенчатую, я бы не стал полагаться на то что могу мозгом обьять всю скрытую глубину задачи во всех ее проявлениях. Я бы сделал наброски применения этой архитектуры, только не в UML, а в коде, который можно гонять туда сюда и смотреть какие есть шороховатости.
Расскажу еще один пример из жизни. Я захотел, чтобы в наш инструментарий по UI тестированию прикрутили возможность генерации тестов на лету. Чтобы написал один тестовый шаблон, кинул в него данные, и меняешь только данные, а не копипастишь код. Такая фишка есть в том же junit, но в нашем инструменте ее нет. Ну так вот, чтобы разрабы смогли ее сделать я им предоставил набор тестов без использования этой функции и аналог с использованием. Т.е им нужно заставить работать мой код. Если все будет сделано правильно оба набора тестов будут выдавать идентичный результат.
В данном применении это вполне инструмент моделирования. Чем писать TLDR спецификации и долго мусолить на совещаниях, куда однозначней: «вот шаблон кода — заставь его работать».
Какой-нибудь сервер может вполне шустро проходить любые тесты, но он свалится, когда, например, к нему пойдут запросы с секундным пингом, канал связи будет перегружен, запросы будут подделываться (ищем отель в Москве, а vpn показывает какой-нибудь Берлин) и т.п.
Сервер не является частью приложения, но частью системы. Как и операционная система, браузер и пр. Нужно четко обозначить границы системы. И в этом вам помогут тесты. Замокать браузер не получится. Это исполняющая среда (Runtime Environment). Но можно обеспечить корректную коммуникацию и реакцию на всевозможные параметры запросов, в точках раздела сред. Тестировать сервер на нагрузку нужно отдельно от приложения. Задержку ответа сервера можно тестировать в модели приложения. Это поможет вам в дальнейшем проще анализировать проблемы, когда ясно на стороне какой из компонент возникает проблема. Но это все сферическая теория в вакууме, на практике для такого подхода просто нет времени.
Но использовать дубли для доступа к внешним ресурсам это не абсолютное правило, если доступ к ним стабилен и достаточно быстр, то можно обойтись и без дублей.
Мне казалось, что основная задача использования дублей — это сокращение «кода под тестами». Т.е. мы хотим проверить работу юнита(как бы масштабно мы его не определяли) — и мы не хотим зависеть от потенциальных проблем со стороны внешних зависимостей. Подключая сь к БД, даже если соединение быстрое и стабильное, мы увеличиваем количество мест, где может произойти ошибка, перестаем тестировать исключительно текущий модуль.
По-моему, в любом случае черезчур общительные тесты (читай использующие внешние ресурсы) — это точно не юнит-тесты (если взаимодействие с ними не единственная отвественность модуля). Всегда понимал Sociable Unit Tests как тесты, которые не мокают/стабят код, который тестируется в соседнем тесте, код, который принадлежит к той же кодовой базе, что и тестируемый.
Это спорный момент, стоит ли делать зависимости или дубли, здесь каждый сам решает для себя, как ему удобнее, и главное, как удобнее для проекта.
Если чутка покопаться, то никакой спорности в этом моменте нет. Разница между доступом к ресурсу и доступом к дублю в поведении: дубль ведёт себя так, как вам нужно. Нужно, чтобы давал правильный ответ — всегда даёт правильный, нужно, чтобы давал отказ — всегда даёт отказ. В отличие от внешних ресурсов, поведение которых по прошествии времени меняется неизбежно.
Многие, очень многие (подавляющее большинство?) разработчиков думают о тестах свысока с позиции текущего момента в разработке: работает/не работает прямо сейчас. Основная ценность же набора тестов, особенно юнит тестов, состоит в нахождении регрессий. И основная ценность дублирования состоит в обеспечении отсутствия ложных регрессий.
От лени всё это. "Да чего дублировать, и так же работает". Ну, сейчас работает. А когда через год или два администраторы базы устроят лёгкий рефакторинг и у вас UI сойдёт с ума на тестах, может уйти куча времени на разбор полётов и устранение проблем. И никто это не отловит заранее, потому что их тесты работают же, а ваши они не смотрят. ;)
Важное различия в юнит тестировании, это какой тип тестирования вы выберите: Solitary (одинокий) и Sociable (общительный) тест.
Если уж говорить про TDD, то часто применяют термин "пирамида тестов" — т.е. не выбирается один или другой подход, а они вполне себе друг друга дополняют. Типичная итерация TDD может выглядеть так:
- написали (расширили базовым кейсом) end-to-end тест самого высокого уровня. Это что-то, что работает с реальным приложением — нажимая кнопки как реальный пользователь, работая с настоящей базой и т.п. и вообще работает с системой как "внешний" наблюдатель. Таких тестов мало, они медленные, и проверяют именно пользовательские сценарии
- на уровне интеграционных тестов (видимо это и есть таинственные "общительные", я никогда такого термина не слышал) написали тест для публичного API. Тут уже какие-то внешние компоненты могут быть замоканы, может использоваться in-memory БД и т.п. Тем не менее, мы работаем с системой через внешний публичный API. Этих тестов уже больше, они относительно быстрые.
- спустились на уровень юнит-тестов. Тут уже никаких вариантов с подключением к базе быть не может, если юнит-тест падает от того, что база недоступна — это очень плохой юнит-тест. Таких тестов может быть очень много, работают они быстро, и работают в изоляции — крайне редко нужно чтобы использовалась реальная внешняя зависимость для юнита.
Проблема "общительных" тестов в применении к юнит-тестам (сам термин не очень корректный и я не слышал, чтобы он вообще использовался) это отсутствие локализации ошибки. В идеале, по упавшему тесту должно быть возможно без всяких неоднозначностей определить, в каком компоненте (функции / классе / компоненте) ошибка. А с наличием зависимостей мы получаем неоднозначность — это наша system under test упала? Или ее зависимость?
Нет, «интеграционные» тесты не общительные, общительные тесты всё ещё юнит‐тесты, никакого (публичного) API не используется. Разница только и исключительно в том, что вы в юнит‐тестах заменили на дубли (mock, dummy, …): в «общительных» тестах вы меняете только лишь тяжёлые вещи (вроде того же подключения к БД). В одиноких вы меняете всё, что можете заменить без того, чтобы тест потерял всякий смысл.
А как проблема «общительных» решается уже писали:
- Вообще‐то юнит тесты быстрые, запускаются часто (зачастую до коммита вообще, если их можно запустить локально), обычно можно запускать конкретные тесты. Если вы поменяли какую‐то тестируемую систему и упал юнит‐тест, то искать «кто виноват» на уровне SUT не нужно независимо от того, какой тест упал: вы поменяли одну конкретную систему. А если вы поменяли сразу много и только потом пустили юнит тесты, то что‐то у вас с процессом разработки не так.
- Если тестируется и некая система, и её зависимости, то вы просто начинаете исправление с зависимостей, а потом перезапускаете тесты. Главное, чтобы тестировалось и то, и то.
Как по мне, то общительные тесты не интеграционные в привычном понимании, а вполне себе юнит, в том смысле, что не используют внешние ресурсы, но в тест-кейсах задействуется не один юнит, а несколько — один целевой и "библиотечные".
Например, есть код типа
class Order
{
public function __construct(Customer $customer) {};
}
Если в тестах мы будем $customer мокать/стабить, то это будет "одинокий" тест, если просто подставим new Customer(), то это будет "общительный". При этом исходим из предположения, что когда дело доходит до тестов класса Order, то класс Customer уже протестирован, и падающие тесты Order будут означать, скорее всего, ошибку в Order.
Таким образом, при использовании юнит тестирования скорость разработки существенно не уменьшаетсяНе понятно как пришли к такому выводу. Перечисленные пункты никакого отношения к выводу не имеют. Стоит ли читать дальше?
Моки, на мой взгляд, стали революцией в юнит тестировании, так как позволили писать тесты только для публичного интерфейса юнита заменив проверку состояния на проверку поведения. Последняя предъявляет более высокие требования к дизайну делая код чище/лучше (можно получить код соответсвующий SOLID принципам ничего не зная о последних).
В TDD c изолированными (Solitary) тестами фаза рефакторинга практически отсутствует (если тесты и юниты достаточно простые) и заменяется проектированием. В классических тестах требования к дизайну минимальны (главное получить нужное состояние на выходе, что можно сделать бесчисленным количеством способов), и в фазе рефакторинга каждый улучшает код как может или не делает этого вовсе. Моки можно сделать строгими задавая ожидаемое поведение, любое неожиданное поведение (вызов не ожидаемого метода или метода с параметром отличным от ожидаемого) приведет к провалу теста. Тесты состояния менее чувствительны к сайд эффектам, так как зависимости могут быть неявными (DI — необязателен). Можно изменить поведение на некорректное таким образом, что тест состояния останется зеленым делая его практически бесполезным. Чтобы этого избежать тест должен запускаться на множестве входных данных, что делает его медленнее.
В связи со всем вышесказанным считаю классические юнит тесты устаревшим подходом, по которому, однако, написано очень много материалов (особенно от авторитетных светил вроде Дяди Боба), что вносит путаницу и уводит новичков от прогресса.
Наличие дублеров делает данную метрику чуть более «честной», так как мы вызываем только тестируемый код.Как по мне то «Наличие дублеров делает данную метрику чуть более «ошибочной»», так как мы вызываем только код одного класса, а не ту связку классов которая будет в продакшине.
То что один юнит будет вызывать другой юнит с заданными параметрами и нужное количество раз как раз таки удобно тестировать с помощью моков. Ну и интеграционные тесты никто не отменяет, просто не нужно оценивать покрытие на основе интеграционных тестов.
(А=1, В=0) либо (А=0, В=1).
А юнит тест в придачу может вызывать случай (А=1, В =1), что в реальности не требуется, но делает покрытие 100%.
Вопрос в том, что такое 100% покрытие. Если одна и та же ветка кода вызывается два раза, это 200% покрытия? Что на самом деле нам говорит покрытие в 100%?
ЧТо каждая значимая строчка кода вызывалась минимум один раз во время прохождения тестов.
Но разве это ценность?
Причем не значимая строчка кода, а просто строчка кода.
Мне думается это не совсем тот результат который я бы хотел.
Мне лично, удобно использовать Solitary (одинокий) UnitTests для TDD, для быстроты разработки. Изолированность класса тут то что надо.
А для тестирования системы необходим инструмент который тыкает на кнопки вместо пользователя, ну или хотя бы дергает сервисы по сценариям пользователя.
Я к тому что процент покрытия тестами это на мой взгляд достаточно бесполезная метрика.
А что в проекте делают незначимые строчки кода? Ухудшают читаемость?
Мои наблюдения таковы, что в проекте, с которым я сейчас больше всего работаю, покрытие обычно отсутствует в коде обработки ошибок, дополнительно часть функциональности унаследована с тех времён, когда тестов ещё особо не писали. Как результат, я уже несколько раз наталкивался на ошибки в коде обработки ошибок, который я случайно или намеренно задействовал в интеграционных тестах других систем. Если бы не было таких «унаследованных» систем, то можно было бы реализовать требование «100 % покрытие», что автоматически означало бы «не забудьте тестировать обработку ошибок». Правда сама цифра мне как‐то без разницы, главное, чтобы обработку ошибок тестировали, это просто простейший вариант заставить контрибьюторов с излишне «позитивным» мышлением делать «негативные» тесты до review.
На практике все программы не идеальные, помимо того что они должны делать, они содержат какие то избытки либо недостатки логики.
Мне довелось работать на медицинском проекте, где по стандартнам необходимо 100% тестирование.
Так вот, в основном это были юнит тесты, которые тестировали не только ту работу, которая необходима для устройства, но и какой то левый непонятный код.
Поскольку был не TDD, то есть не сначала тесты, а сначала несколько лет писалась логика, потом, перед сдачей, выделили время на покрытие тестами.
Как это делалось — бралась ветка if(condition){block1}else{block2} и под условия писался юнит тест.
Что тестировал этот тест?
Все что мог, в том числе излишнюю логику и даже замораживал, а не тестировал, код тех баг, которые неосознанно в код внес программист.
Это была заморозка логики, а вовсе никакой нифига не тест.
Поскольку программы пишутся не минимальные то и 100% покрытия это абсурд.
Это была заморозка логики, а вовсе никакой нифига не тест.
Это и есть основное назначение юнит-тестов :) При практически идеальном покрытии кода этими тестами они упадут при малейшей попытке изменить логику. Они являются красным индикатором "ЛОГИКА ИЗМЕНЕНА!!!". А желательное это изменение ил инет — решать разработчику. В перовом случае править тесты, во втором — код.
Так для недопущения таких ситуаций reviewer’ы потом смотрят и на код, и на тесты и спрашивают «а чего это ваш код ведёт себя именно так?» в особых случаях. С полным покрытием такие «особые случаи» можно легче заметить по тестам, без полного покрытия их и не заметит никто. Если нет людей, которые проверяют код, то тесты в любом случае не сильно помогут. Только стремиться к 100 % покрытию функциональными, а не юнит, тестами с этой точки зрения имеет наибольший смысл: видно, что код реально может быть вызван, а понять, адекватно ли поведение, легче.
стремиться к 100 % покрытию функциональными, а не юнит, тестамиЮнит тесты, тоже функциональные. Вы наверно имеете в виду интеграционные тесты и выше — System и Acceptance
Эм, приёмочные тесты и есть функциональные, сравните определение в https://en.wikipedia.org/wiki/Functional_testing («a type of black-box testing that bases its test cases on the specifications of the software component under test») и https://en.wikipedia.org/wiki/Acceptance_testing («a test conducted to determine if the requirements of a specification or contract are met»). В https://ru.wikipedia.org/wiki/Разработка_через_тестирование вообще прямо написано
Приёмочные (функциональные) тесты (англ. customer tests, acceptance tests) — тесты, проверяющие функциональность приложения на соответствие требованиям заказчика.
Не учитываются в покрытии строки комментариев, строки деклараций и т. п. "Значимая" тут -"исполняемая".
Юнит-тесты — не инструменты выявления ошибок, не инструменты тестирования системы или пользовательских сценариев, это лишь инструмент фиксации желаемого поведения отдельных кусков кода.
Я лично считаю, что юнит тесты должны быть "одинокими", в том числе и потому что так метрика покрытия кода юнит-тестами даёт более полезное значение — автор теста хотел покрытыть тестами именно покрытую строку, а не она "сама" покрылась, может даже без ведома автора.
Юнит-тесты — не инструменты выявления ошибок, не инструменты тестирования системы или пользовательских сценариевВсе таки, если говорить о TDD, то это хотя бы инструмент выявления недоработок.
это лишь инструмент фиксации желаемого поведения отдельных кусков кода.Не только желаемого, но и инструмент фиксации случайно протестированных багов.
Все таки, если говорить о TDD, то это хотя бы инструмент выявления недоработок.
Не согласен. При TDD мы сначала фиксируем желательное поведение в тестах, а потом добиваемся его от кода. В конце рефакторм так, чтобы поведение не изменилось.
Не только желаемого, но и инструмент фиксации случайно протестированных багов.
Фиксация желаемого поведения заключается в коде теста. Если вы ошиблись в коде теста, значит вы ошиблись в выражении того, какое поведение вы считаете желаемым. Как говорится "телепаты в отпуске", а "написанное пером не вырубишь топором" :)
Если вы ошиблись в коде теста, значит вы ошиблись в выражении того, какое поведение вы считаете желаемым.Вот кстати, ведь в реализации метода можно использовать больше или меньше условий чем подразумевает тест.
Инструменты покрытия, считают процент только по строкам кода, или еще и по количеству комбинации условий в операторе IF?
Юнит тесты. Первый шаг к качеству