Если вы ответственный разработчик, то напишите тесты сами. Так поступают все ответственные разработчики. Но что вы будете тестировать?
Возможно, вы захотите убедиться в том, что:
- Функции будут вести себя должным образом, несмотря на неправильные аргументы – например, null, или другие неожиданные типы аргументов.
- Необязательные параметры функций имеют правильные значения по умолчанию — в тех случаях, когда эти значения вызывающая сторона не указывает.
- Функция расположена в правильном модуле, и её можно вызвать.
Конечно, вы можете много чего проверить. Например, делает ли код то, что он, по идее, должен. По моему опыту, вне зависимости от языка, такие тесты необходимы. Но это не всё, о чем я сегодня хочу поговорить.
Дополнительно можно убедиться в том, что код:
- Не имеет синтаксических ошибок.
- Работает во всех средах, которые вам интересны (к примеру, в разных браузерах).
- Правильно использует библиотеки (например, запутанные ORM APIs).
Вы не прогадаете, если будете использовать линтер. Правда, его помощи недостаточно.
Разработчики часто пытаются протестировать весь код. Они берут инструменты, которые помогают проследить, какие строки кода выполняются во время тестов. Полноценная проверка означает, что каждая строчка кода будет выполнена как минимум одним из тестов.
С языками программирования, которые я перечислил выше, всегда приходится тестировать всё по-максимуму. Если у вас мало тестов, то готовьтесь к диалогу с персональным чертенком: «Ты помнишь, что у тебя нет компилятора? Твой компилятор – это твои тесты. Либо все протестируешь, либо выпустишь некомпилирующийся код. Не надо быть плохим разработчиком!»
Я склонен прислушиваться к этому голосу – потому что не хочу быть плохим разработчиком.
Периодически я глубоко дышу – с надеждой прозреть. И когда я это делаю, мне становится ясно, что я замешан, возможно, в крупнейшей растрате сил в истории человека.
Я привык писать код на C++ и Java. Как раз у них есть что-то под названием «компилятор». То есть инструмент, который, кроме всего прочего, проверяет код на наличие синтаксических ошибок. До того, как программа запустится в первый раз. Это всё ещё новинка в мире веб разработки.
К компилятору можно относиться как к простой системе юнит-тестирования. Она гарантирует, что:
- 100% вашего кода будет без синтаксических ошибок;
- 100% вызовов функций будут вызывать функции. Причем те, которые существуют.
Если изменяется публичный API библиотеки низкого уровня, ваш компилятор не забудет сообщить вам об этом до того, как вы запустите программу. Если вы переименуете функцию, компилятор подскажет, что нужно обновить имя также там, где она вызывается.
Для каждой функции, которую я писал на JavaSript/Python/Ruby/PHP, мне приходилось писать юнит-тесты, чтобы всё это проверить. Даже не так. Хуже. Я писал отдельный тест для каждого места вызова функции, который проверял, вызывается ли она в этом месте. Чтобы вы поняли – если у меня есть функция, которая вызывается в 10 разных местах моего кода, я должен протестировать не только функцию, но ещё и все эти десять мест.
Примечание переводчика: на самом деле это не так и юнит тест тестирует “юнит”. Но в разрезе статьи, если рассматривать юнит-тесты как способ добавить статических проверок для динамических языков, можно сделать такое допущение.
Это O(N), ребята.
Если какой-нибудь разработчик будет рефакторить мой код, и мои тесты перестанут тестировать те же пути выполнения кода, тогда я потеряю преимущества этих тестов и никогда об этом не узнаю. Прости, брат.
Братья-программисты говорят «Прости» и пожимают плечами.
Компилятор полностью заменяет юнит-тесты? Не совсем. Смысл в другом. Если мы выбираем язык программирования без компилятора и компенсируем это написанием множества тестов, мы повторяем то, что уже сделали несколько очень крутых разработчиков компилятора. Просто подумайте. Если я пишу код на Go, то из тестов, которые мне не нужно писать, можно сложить небольшую гору. Разработчики компилятора на Go уже сделали это за меня – для каждого проекта на Go, который будет существовать.
Расскажу забавную историю. Несколько лет назад мой друг переключился с JavaScript на Go. Он написал немного кода, а потом попытался написать тест, который проверил бы, что код корректно себя ведет с неправильным типом. Он долго боролся с компилятором. Пока не осознал, что такая ситуация, в принципе, невозможна.
Не утверждаю, что Go – единственный правильный язык. Так работают все языки, у которых есть компилятор. Даже языки с ужасной системой типов, как C++, тоже так работают. Вам не нужно быть опытном Haskell-разработчиком, чтобы получить все бонусы от этой концепции.
...
Всегда, когда пишете тест, подумайте: если компилятор может сделать проверку за вас, нужно ли вам это делать? Отложите работу и задумайтесь о жизненных приоритетах. О тех умных авторах компиляторов. Они пытаются вам помочь. Возможно, стоит им это позволить.
Эпилог.
Дальше можете не читать.
Я предупредил.
На счастье JavaScript-разработчиков, сообщество увидело надвигающуюся угрозу — и предотвратило её с помощью инструментов Flow и TypeScript. Но поскольку эти инструменты используют постепенное типизирование, вы никогда не сможете быть полностью уверены в том, что каждая строчка кода проекта “берет всё” от системы типов. Да, они помогают. Но поскольку они интероперабельны (или являются надстройкой над) JavaScript, вы не нуждаетесь в 100% «страховочной сети», которую компилируемый язык дает вам.
Более продвинутые системы типов могут пойти дальше. Например, Ada может обеспечить проверку значения числовых переменных на этапе компиляции. Даже может предотвратить индексирования массива числом, которое заведомо больше, чем размерность массива. На этапе компиляции.
Haskell тоже имеет несколько мощных концепций типизации. Тип Maybe говорит компилятору, что значение может быть пустым (аналог null). И любой код, который использует это значение, должен быть уведомлен об этом, или возникнет ошибка компиляции.
Средства сборки JavaScript webpack and Browserify могут сильно помочь с этой проблемой. Если у вас нет проверки типов на этапе компиляции. Например, если у вас есть синтаксическая ошибка в вашем JavaScript, webpack выбросит ошибку во время создания бандла. Это очень полезно.
Поздравляю тех, кто читал между строк, и понял, о чём эта статья. Здесь были завуалированы громкие слова против динамически типизированных языков. На самом деле, я спокойно отношусь к этим языкам. Но негативные побочные эффекты при тестировании могут дорого обходиться разработчику. Конечно, есть другие негативные эффекты, которые делают статически типизированные языки затратными: время сборки, излишне многословные интерфейсы, поиск путей, где/что лежит и так далее. Некоторые статически типизированные языки (Go) решают эти неудобства лучше, чем другие (С++).
Комментарии (25)
TheShock
08.08.2016 13:57+5Зачем писать юнит-тест на неправильное использование чего-либо? Можно придумать стремящееся к бесконечности число способов неправильного использования любого мало-мальски большого кода.
Нам необходимо проверить, что наш код работает корректно, а не что если мы напишем некорректный код — он будет работать корректно. Вот и стоит писать тесты на приложение. И негативные тесты только там, где реально может прийти что-либо неожидаемое. Должны упасть если неправильный ввод пользователя или недостаточно данных? Окей. Но проверять, что функцияanalyzeString()
будет падать, если передать в нее массив, объект, число или яблоки — моветон. Все-равно свалятся более высокоуровневые тесты кода, который так странно использует эту функцию.TheShock
08.08.2016 13:58пс. Хотя да, статическая типизация — огромное преимущество. И не только из-за тестирования.
lair
08.08.2016 14:08Зачем писать юнит-тест на неправильное использование чего-либо?
Чтобы удостовериться, что при неправильном использовании система не пожирает галактику.
LionAlex
08.08.2016 14:46-2Не стремящееся к бесконечности, а ровно бесконечность. Но на то и существуют классы эквивалентности, чтобы сократить число подобных вариантов к минимуму.
deniskreshikhin
08.08.2016 15:03+1Да, это еще называют «оборонительное программирование» (defensive programming).
Многие начинающие программисты рассматривают функцию как некоторую непреступную крепость, и каждый вызов функции это попытка сломать эту крепость. Поэтому каждый аргумент поделжит тчательной проверки.
LionAlex
08.08.2016 14:43+12Ну офигеть, а то мы не знали преимуществ статической типизации.
А ведь вместо написания статей, автор мог бы научится нормально писать юнит-тесты.poxu
08.08.2016 15:34Вы слышали про такого человека, как Роберт Мартин? Он же дядя Боб. Вот он утверждает, что если к динамичекому коду добавить юнит тесты, то у статистически типизированного кода не будет преимуществ перед динамически типизированным кодом.
LionAlex
08.08.2016 15:50Вот тут главное не забывать, что стопроцентное покрытие кода тестами в реальном проекте недостижимо за приемлемое время, да к тому же само по себе ничего не гарантирует с точки зрения работоспособности программы.
poxu
08.08.2016 20:33Роберт Мартин вот утверждает, что 100% это как раз реальные цифры. И что отсутствие стопроцентного покрытия гарантирует проблемы.
playermet
09.08.2016 09:23Роберт Мартин вот утверждает, что 100% это как раз реальные цифры
Цитату можно?
И что отсутствие стопроцентного покрытия гарантирует проблемы.
Каким образом?poxu
09.08.2016 10:32Цитату можно. Вот она.
Am I suggesting 100% test coverage? No, I’m demanding it. Every single line of code that you write should be tested. Period.
Что касается того, каким образом отсутствие стопроцентного покрытия гарантирует проблемы — у Мартина вообще много про это. Книги Clean Code и Clean Coder этого касаются. Наверное можно вот эту статью почитать.
playermet
09.08.2016 11:28Цитату можно. Вот она.
Так он не говорит, что это реальные цифры. Он говорит что настаивает на их достижении. На практике мало кому это удается.
Что касается того, каким образом отсутствие стопроцентного покрытия гарантирует проблемы — у Мартина вообще много про это.
Не знаю что там у Мартина, но по законам формальной логики отсутствие проверки не может гарантировать наличие ошибки. А кроме того стопроцентное покрытие не гарантирует отсутствие проблем (а лишь отсекает их часть).
arvitaly
09.08.2016 06:48+1Дядюшка говорит о том, что нет других способов поставить задачу кроме тестов. Текстовое или устное описание ТЗ он не считает за требования. С его точки зрения нельзя говорить о не 100% покрытии кода тестами, ведь только код, написанный для этой задачи (определенной тестами) и является кодом ЭТОГО проекта. А остальной код он отношения не имеет к делу и чем он там покрыт не важно.
Собственно, тесты и определяют «работоспособность».
Тут фишка заключается в том, что если заказчик дает вам задания не в виде формальных спецификаций (это те, которые трактуются однозначно), то вы должны их написать сами или не писать код вовсе.
А код, написанный без таких требований это интуитивная попытка угадать их, не формализуя, а вернее формализуя где-то в подсознании. Отсюда и все последующие проблемы — «не угадал», «угадал, но немного не так». Обобщая, вы становитесь «частью заказчика».
TheShock
08.08.2016 20:47-1у статистически типизированного кода не будет преимуществ перед динамически типизированным кодом.
Я вот не могу согласиться. Кроме тестирования это ещё локальная самопроверяющаяся документация и хорошая поддержка ИДЕ. Это кое-как можно сделать через всякие там JSDoc, но это не так удобно и не так хорошо поддерживается.poxu
08.08.2016 22:10Я тут тоже с Мартином не согласен. Но сам факт существования подобной точки зрения однозначно говорит, что статьи о том, что статическая типизация полезна — нужны.
vobo
08.08.2016 16:48+2В динамических языках можно платить за проверки (написанием большего числа тестов), только если они нужны. Полно случаев, когда вполне можно обойтись без них, написав код без траты времени на продумывание типов
arvitaly
08.08.2016 17:13+1Все дело в повторном использовании и написании интерфейсов в связи с этим, во всех остальных случаях всегда можно вывести и проверить типы автоматически.
tmn4jq
08.08.2016 21:44Для автора, видимо, смысл юнит-тестов заключается в том, чтобы проверить ту или иную функцию, да и вообще что код запустится. В то время как юнит-тестирование не означает исключительно тестирование функции на разных параметрах, это что-то пошире. Само слово unit-testing говорит, что это тестирование модуля и его поведения. Следовательно, тестировать разумно результат, а не сам процесс, ведь одного и того же результата можно достигнуть множеством путей. Считаю, что автор статьи забыл добавить три важных слова: «По моему мнению».
Да, под автором я понимаю именно автора, а не переводчика, которому спасибо)
Alexey2005
08.08.2016 21:50Мне больше нравится тот замечательный бонус к компилируемым языкам программирования, который называется линкер. Представьте, что в ваш проект попадают те и только те участки кода, которые реально используются. Это же так здорово!
В случае же интерпретируемых языков всё не так радужно. Используете всего пару функций из jQuery? Ваш проект поправится на полный объём этой библиотеки. Используете всего 5% тех возможностей, которые предоставляет AngularJS? Ваш проект неминуемо вырастет сразу на полметра. И так может дойти до того, что простенькая страничка тянет за собой 10Мб зависимостей, из которых реально выполняется порядка 40Кб. Ну не бред ли?Fedcomp
08.08.2016 22:13+1И тут на сцену выходит webpack tree-shaking
P.S. Автору статьи надо бы узнать про TDD, а еще про моки.
lair
Правда? Или, может быть, все-таки "мне не нужно писать некоторые тексты"?
lair
Так и есть. В оригинале:
Не "мне не нужно писать тесты", а "некоторые тесты мне никогда не придется писать".
om2804
Мне на секунду показалось, что компилятор Go имеет искусственный интеллект :)
poxu
Маленькое уточнение. Это предложение переводится как — Если я пишу код на Go, то из тестов, которые мне не придётся писать можно сложить небольшую гору.
Является эта небольшая гора всеми тестами, или только некоторыми можно понять только из контекста. И из контекста ясно, что речь идёт о некоторых тестах, а не обо всех.
eyeofhell
Разумно, переделал.