Юнит тесты Vs функциональные тесты — взгляд руководителя и разработчика
Современную разработку программного обеспечения тяжело представить без автоматического тестирования — по сути это единственный способ защитить продукт от разрушительных изменений (то есть изменений, повреждающих существующий функционал).
Обычно используют два вида автоматических тестов:
Модульное тестирование (тестирование отдельных частей продукта, обычно отдельных функций/методов)
Функциональное тестирование — тестирование некого функционала продукта, при этом продукт воспринимается как единый «чёрный ящик».
Но давайте зададим интересный вопрос — действительно ли нужны оба вида тестирования сразу, и если нет — то какое из них важнее?
Итак, для начала модульное тестирование.
Кто пишет? Как правило автор модуля/метода/функции, т. к. обычно только он знает, что данная функция ДОЛЖНА делать. Как правило, это разработчик, и его цель — покрытие кода юнит-тестами. А это значит, что скорее всего, юнит-тест будет весьма не полным (как правило разработчики проверяют лишь работу с корректными данными, либо добавляют небольшой набор некорректных данных, т. к. у них не стоит задачи «сломать» свой код), либо, если разработчик действительно ответственно подойдёт к процессу написания юнит-теста, то разработка займёт вдвое больше времени. Не верите? А ведь только качественное покрытие функции «является ли аргумент числом» требует более 10 проверок. К тому же, существует огромное число функций, проверить которые модульным тестированием невозможно, либо очень сложно (это функции, поведение которых зависит от состояния системы в целом).
И даже правильная работа всех модулей системы, отнюдь не гарантирует их правильное взаимодействие.
Ещё один момент, если у вы хотите ввести модульное тестирование в продукт, на котором оно раньше не использовалось — то это очень серьёзные трудо-затраты, и покрытие небольшого числа функций абсолютно ничего не даёт.
Самое главное — даже успешное прохождение всех юнит-тестов не гарантирует правильной работы продукта: ведь одна и та же функция может быть использована в различных частях системы, в то время как юнит-тест писался для неё с оглядкой лишь на один вариант использования.
Простой пример: допустим у нас есть функция checkInt(a)
checkInt(a)
В моей практике даже был вариант комментирования моего юнит-теста, т. к. он завершался неудачно, после изменения моей функции.
Однако, следует признать, что модульное тестирование великолепный инструмент, если вы пишите некую библиотеку, предназначенную для использования многими разработчиками — в этом случае модульные тесты будут выполнять и роль документации к функциям (ведь не секрет, что мы часто пропускаем описание функции и сразу смотрим примеры использования). Впрочем, в данном случае, сама библиотека является продуктом, и модульные тесты не отличимы от функциональных.
Теперь вернёмся к функциональным тестам. Как правило, они пишутся тестерами. У которых стоит задача найти ошибку (по крайней мере своим тестерам мы всегда ставим такую задачу). А значит будет больше проверок на нестандартные данные — что согласитесь, просто великолепно! К тому же, мы можем разделить правами доступов код продукта и тестов, что позволит избежать изменений тестов «чтоб он был зелёный, потому что надо релизиться»
К тому же, функциональными тестами гораздо проще покрывать готовый продукт, чем модульными — т. к. гораздо проще понять что конкретно должна и не должна делать определённая часть пользовательского интерфейса, чем определить что ДОЛЖНА делать данная функция. И самая большая прелесть — вы можете начать покрывать функциональными тестами только самые важные части продукта — и они будут исправно гарантировать их работоспособность.
Вам не нужны юнит-тесты
Да, вы не ослышались – именно так! В IT-сообществе прочно укоренилось мнение, что все эти тесты вам хоть как-то помогают, но так ли это на самом деле? Вы сами пробовали мыслить критически и анализировать это расхожее мнение? Хипстеры придумывают кучу парадигм – TDD, BDD, ПДД, ГИБДД – лишь чтобы создать иллюзию бурной деятельности и хоть как-то оправдать свою зарплату. Но задумайтесь, что будет, если вы (либо ваши программисты) начнете все свое время уделять исключительно написанию кода? Для тестирования есть отдельное направление и целые подразделения. Вы же не заставляете программистов писать требования, так? Тогда почему они должны писать тесты? Всех согласных и несогласных прошу проследовать внутрь поста, где я вам наглядно покажу, что юнит (и интеграционные) тесты – великое зло!
Откуда вообще пошло тестирование
В стародавние времена никакого тестирования не было в принципе. Не было даже такого направления, что уж и говорить про такие термины, как блочное (модульное) и интеграционное тестирование. А про всякие e2e и, прости господи, пайплайны, я вообще молчу. И все это потому, что тестировать, собственно, было еще нечего. В те годы инженеры-программисты только пытались создать первые ЭВМ.
Как нам всем известно, первые ЭВМ были гигантских размеров, весили десятки тонн и стоили дороже этих ваших Apple MacBook Pro Retina 4k 32Gb RAM 1Tb SSD Touch Bar USB Type-C. И в те времена разработчики действительно боялись, что во время работы что-нибудь пойдет не так. Думаю, вам известна история возникновения термина «баг» (bug) – если вдруг нет, то почитайте, это очень интересно. И, так как программисты боялись всего на свете, они и придумали модульное тестирование.
Времена менялись, менялись и ЭВМ. Тестирование тоже менялось. Помимо блочных тестов, возникло также и целое направление, которое впоследствии получило название Quality Assurance.
Но разработчики тоже менялись. В наше время становится смешно от мысли, что кто-то боится запустить программу, потому что от этого может загореться сервер. В 2020 году программисты не боятся запускать свои программы. А если нет страха – зачем тестировать?
Современные реалии
Повторю свой вопрос – если ваш MacBook (или Xiaomi) не взорвется из-за ошибки в коде, зачем тогда тестировать? Вы просто запускаете и наслаждаетесь результатом. Предвосхищая ваше негодование по поводу дороговизны ошибок для заказчика – пускай тестированием занимаются специально обученные люди.
На последнем хочу слегка заострить внимание. В современной разработке основная стоимость кроется не в аппаратном, а в программном обеспечении. И ошибки по-прежнему стоят дорого. Но ответственность за эти ошибки плавно перекочевала с плеч разработчиков на плечи тестировщиков. Как-никак, это они назвали себя Quality Assurance – а раз проводишь проверку качества, делай это качественно ¯\_(ツ)_/¯
В конце концов, отдел разработки называется Software Development, а не Unmistakable Development. Мы никому ничего не обещаем.
Хороший программист уверен в себе
Когда вы покрываете свой код юнит-тестами, вы будто заявляете всему миру: «Смотрите, я не уверен в том, что оно работает». Будут ли вас за такие мысли уважать более опытные коллеги и начальник? Будет ли вам доверять заказчик?
Просто откройте свой проект и задумайтесь. Вы – умный и образованный человек. Вы хороши собой. Зачем вам балласт в виде модульных тестов, которые, ко всему прочему, еще и портят вашу репутацию?
Задание: Прямо сейчас скажите себе «Я уверен в качестве своего кода» и удалите все юнит-тесты из проекта.
В идеальном случае, вы должны удалить все тесты прямо из ветки master, однако, это может быть запрещено правилами репозитория. В таком случае вам придется делать pull request и убеждать своих старомодных коллег в том, что тесты – пережиток прошлого. Однако, как только ваши соратники примкнут к вашему мировоззрению, на проекте воцарится гармония.
Запомните несколько простых постулатов:
Хороший программист не пишет тесты, так как не сомневается в качестве своей работы.
Превосходный программист даже не компилирует код перед тем, как запушить изменения на прод. Нет, дело не в том, что он настолько крут, что компилирует все в уме, вовсе нет. Просто он, опять-таки, не сомневается в своих знаниях и качестве своего кода.
Тщетные попытки найти ошибки в вашем коде оставьте тестировщикам.
Тесты отнимают время
Время программистов – дорогое. Время тестировщиков – дешевое. Какой тогда смысл заставлять программистов писать тесты? Это невыгодно даже с финансовой точки зрения.
Ученые давно выяснили, что постоянное переключение между контекстами истощает ресурсы мозга. Если вы погонитесь за двумя зайцами одновременно, ни одного не поймаете. Вы не будете продуктивны ни в написании кода, ни в написании тестов. А если же вы будете все свое время посвящать исключительно программированию, ваша продуктивность устремится вверх по экспоненте.
В конце концов, тестировщики тоже люди, и не стоит лишать их хлеба. Если вы будете сами тестировать свой код, весь отдел QA станет попросту не нужен, и бедолаг уволят. Вспомните, что было во времена промышленного переворота в США – когда машины начали заменять людей на производстве, начались самые настоящие бунты.
Поэтому – не будьте машиной. Не провоцируйте тестировщиков на поднятие бунта.
Парадигмы запутывают
Unit-testing, Integration Testing, End 2 End, Pipelines, CI, CD – что вы еще придумаете, лишь бы не работать? Есть мнение, что когда программист выгорает и начинает прокрастинировать, он идет настраивать пайплайн.
Все эти парадигмы и термины запутывают. Опять же, ваши мозговые ресурсы мгновенно тратятся, когда вы начинаете думать про e2e вместо того, чтобы просто программировать. Все это пугающе отвлекает вас и не дает сосредоточиться на главном – на написании кода.
Если кому-то надо настроить CI или CD – пускай настраивают сами. Пусть это сделает devops, в конце концов. Если вас будут просить как-либо помочь в настройке, смело отказывайтесь и ссылайтесь на свою занятость наиважнейшими и перво-приоритетными задачами, а именно – написанием кода.
Вам не нужно знать ничего лишнего. Иными словами, вы – программируете. Тестировщики – тестируют. Девопсы – ковыряются во всяких скриптах на bash. Менеджеры… ну, менеджеры есть менеджеры.
Delivery In Time
Я предлагаю ввести лишь одно простое понятие: DIT – Delivery In Time. Это схоже с известной парадигмой ППКБ (Просто Пиши Код Б****), но звучит гораздо современнее и толерантнее. Парадигма ППКБ ставит программистов в центр мироздания и не считается с работой других членов команды. Это, как минимум, неуважительно. В DIT мы верим, что программисты – скромные служители, единственной целью которых является написание кода. При всем этом, мы не закрываем глаза на работу других коллег и уважаем их труды. Просто мы считаем, что каждый должен быть занят своим делом: программисты – программировать, тестировщики – тестировать, и тд. Когда каждый будет делать то, чему обучен, сроки перестанут срываться.
Парадигма DIT предлагает сплошные бонусы заказчикам. Они могут нанять исключительно разработчиков, чтобы те ППКБ (просто писали код), и все их бюджеты будут направлены непосредственно на создание продукта. При желании заказчик может также нанять и тестировщиков. То есть, простите, Quality Assurance инженеров. А может и не нанимать и запустить тестирование в продакшене.
Я однажды слышал один забавный диалог:
– Сколько человек сейчас тестирует нашу систему?
– Один человек.
– Мы только что выкатили ее на прод.
– Ну… значит, нашу систему тестирует 1000 человек.
И это правильно. Можете платить штатным тестировщикам, а можете нанять тысячи внештатных совершенно бесплатно.
При этом стоит держать в голове, что парадигма DIT гарантирует, что программный код будет написан вовремя, потому что разработчики будут заняты только своей работой. Но если подключать тестировщиков, то сроки сдвинутся как минимум на время их работы. А если они найдут какие-то баги (что, разумеется, маловероятно, ведь хорошие программисты уверены в своей работе), то сроки сдвинутся еще немного.
Совет: Чтобы не срывать сроки и доставлять вовремя – лучше нанять разработчиков, а тестированием заниматься на продакшене. Даже если что-то пойдет не так, вы всегда можете возразить, что соблюли сроки, как и было обговорено. А о большем и не договаривались.
Про интеграционное тестирование
С модульным тестированием вроде разобрались, настало время поговорить о тестировании интеграционном. Именно оно отнимает больше всего времени.
Когда-то я был молодым и верил в то, что тесты (юнит, интеграционные, да всякие) несут добро. Хорошо написанные тесты гарантировали отсутствие регрессии, то есть вы могли изменять и рефакторить код без боязни, что вы где-то ошиблись. Выглядит здорово, правда? Делаешь кучу правок, запускаешь тесты и смотришь, допустил ли ты ошибку.
Но теперь я повзрослел. Я зрю в корень проблемы, а не на ее последствия. И корнем проблемы является человек по ту сторону монитора, в то время как ошибки в тестах – лишь ее последствия. Если улучшить, прокачать навыки программиста, то проблема решится естественным образом, и любые дополнительные проверки утратят актуальность.
Всю эту привычку гонять тесты я могу сравнить с неуверенным в себе поваром в ресторане. Представьте на минутку, что вы шеф, и приготовили фирменное блюдо ресторана. Вы уверены в том, что оно вкусное? Может, вам стоит прогнать тесты и, скажем, бросить кусок блюда ресторанной собаке? Право же, смешно. Разумеется, вы просто берете и гордо выносите блюдо клиентам без каких-либо опасений. Вы – профессионал и уверены в своей работе.
Но я отошел от темы. Интеграционные тесты (кстати, е2е тоже) – это большие усилия. Вы поднимаете несколько компонентов с полными контекстами, или даже несколько систем, и начинаете пересылку сообщений из одной в другую. Вы продумываете тестовые сценарии, причем меньше положительных и больше отрицательных, чтобы удостовериться в работоспособности кода. Вы начинаете думать как тестировщик и вести себя как тестировщик. Вы на грани.
Понимаете, к чему я клоню? Вы делаете чужую работу и отнимаете чужую зарплату. Вы становитесь машиной. А я уже напоминал, к чему может привести промышленный переворот.
Просто будьте собой
Будьте собой и будьте счастливы. Определите свои ценности и следуйте выбранному курсу. Цель программиста – созидать, в то время как цель тестировщика – разрушать. Вы не можете создавать что-то новое лишь для того, чтобы это затем разрушить. Ведь тогда вы вступите в конфликт с самим собой, со своей сущностью. Именно поэтому вам не следует писать тесты на свой собственный код.
Просто будьте собой!
В качестве заключения
Если вы дочитали до этого момента и не бросились писать гневный комментарий, то либо вы прекрасно понимаете важность тестов и сразу заметили иронию, либо просто обратили внимание на теги 🙂
Друзья, это были вредные советы. Однако, навеяны они были реальными историями. Сейчас у меня на рабочих проектах хорошее покрытие, и это действительно сильно облегчает работу. Но однажды, давно, я попал на проект с покрытием бранчей в районе 15%. Мы не вылезали из регрессии. Примерно тогда я начал осознавать всю важность тестов и стал задумываться о том, почему некоторые из нас ими пренебрегают.
А какой процент покрытия в ваших проектах? Дотягивает ли покрытие линий/веток до 80%? Или болтается где-то в районе 30? Если у вас частая регрессия и низкое покрытие – вы догадываетесь, что стоит изменить?
Я понимаю, что подобный пост не совсем по тематике Хабра. Но сегодня пятница, к тому же на носу Новый Год, так что давайте немного расслабимся 🙂
И, пользуясь случаем: если вам понравился такой формат, то приглашаю вас в свой блог плохого разработчика (часто лежит) паблик вк, где я и выкладываю время от времени подобные саркастические посты, которые зачастую неудобно выкладывать сюда.
Unit-тесты, пытаемся писать правильно, чтобы потом не было мучительно больно
100 модульных тестов и приходится тратить продолжительное время на то чтобы заставить их работать вновь. Итак, приступим к «хорошим рекомендациям» при написании автоматических модульных тестов. Нет, я не буду капитаном очевидностью, в очередной раз описывая популярный стиль написания тестов под названием AAA (Arange-Act-Assert). Зато попытаюсь объяснить, чем отличается Mock от Stub-а и что далеко не все тестовые объекты — «моки».
Глобально модульные тесты можно условно поделить на две группы: тесты состояния (state based) и тесты взаимодействия (interaction tests).
Тесты состояния — тесты, проверяющие что вызываемый метод объекта отработал корректно, проверяя состояние тестируемого объекта после вызова метода.
Тесты взаимодействия — это тесты, в которых тестируемый объект производит манипуляции с другими объектами. Применяются, когда требуется удостовериться, что тестируемый объект корректно взаимодействует с другими объектами.
Стоит также заметить, что модульный (unit) тест может запросто превратиться в интеграционный тест, если при тестировании используется реальное окружение(внешние зависимости) — такие как база данных, файловая система и т.д.
Интеграционные тесты — это тесты, проверяющие работоспособность двух или более модулей системы, но в совокупности — то есть нескольких объектов как единого блока.
В тестах взаимодействия же тестируется конкретный, определенный объект и то, как именно он взаимодействует с внешними зависимостями.
Внешняя зависимость — это объект, с которым взаимодействует код и над которым нет прямого контроля. Для ликвидации внешних зависимостей в модульных тестах используются тестовые объекты, например такие как stubs (заглушки).
Стоит заметить что существует классический труд по модульным тестам за авторством Жерарда Месароша под названием «xUnit test patterns: refactoring test code«, в котором автор вводит аж 5 видов тестовых объектов, которые могут запросто запутать неподготовленного человека:
— dummy object, который обычно передается в тестируемый класс в качестве параметра, но не имеет поведения, с ним ничего не происходит, никакие методы не вызываются. Примером таких dummy-объектов являются new object(), null, «Ignored String» и т.д.
— test stub (заглушка), используется для получения данных из внешней зависимости, подменяя её. При этом игнорирует все данные, могущие поступать из тестируемого объекта в stub. Один из самых популярных видов тестовых объектов. Тестируемый объект использует чтение из конфигурационного файла? Передаем ему ConfigFileStub возвращающий тестовые строки конфигурации для избавления зависимости на файловую систему.
— test spy (тестовый шпион), используется для тестов взаимодействия, основной функцией является запись данных и вызовов, поступающих из тестируемого объекта для последующей проверки корректности вызова зависимого объекта. Позволяет проверить логику именно нашего тестируемого объекта, без проверок зависимых объектов.
— mock object (мок-объект), очень похож на тестовый шпион, однако не записывает последовательность вызовов с переданными параметрами для последующей проверки, а может сам выкидывать исключения при некорректно переданных данных. Т.е. именно мок-объект проверяет корректность поведения тестируемого объекта.
— fake object (фальшивый объект), используется в основном чтобы запускать (незапускаемые) тесты (быстрее) и ускорения их работы. Эдакая замена тяжеловесного внешнего зависимого объекта его легковесной реализацией. Основные примеры — эмулятор для конкретного приложения БД в памяти (fake database) или фальшивый вебсервис.
Roy Osherove в книге «The Art of Unit Testing» предлагает упростить данную классификацию и оставляет всего три типа тестовых объектов — Fakes, Stubs и Mocks. Причем Fake может быть как stub-ом, так и mock-ом, а тестовый шпион становится mock-ом. Хотя лично мне не очень нравится перемешивание тестового шпиона и мок-объекта.
Запутанно? Возможно. У данной статьи была задача показать, что написание правильных модульных тестов — достаточно сложная задача, и что не всё то mock, что создаётся в посредством mock-frameworks.
Надеюсь, я сумел подвести читателя к основной мысли — написание качественных модульных тестов дело достаточно непростое. Модульные тесты подчиняются тем же правилам, что и основное приложение, которое они тестируют. При написании модульных тестов следует тестировать модули настолько изолированно друг от друга, насколько это возможно, и различные разновидности применяемых тестовых объектов в этом, безусловно, помогают. Стиль написания модульных тестов должен быть таким же, как если бы вы писали само приложения — без копипастов, с продуманными классами, поведением, логикой.
Зачем нужны юнит-тесты
Авторизуйтесь
Зачем нужны юнит-тесты
Многие разработчики говорят о юнит-тестах, но не всегда понятно, что они имеют в виду. Иногда неясно, чем они отличаются от других видов тестов, а порой совершенно непонятно их назначение.
Доказательство корректности кода
Автоматические тесты дают уверенность, что ваша программа работает как задумано. Такие тесты можно запускать многократно. Успешное выполнение тестов покажет разработчику, что его изменения не сломали ничего, что ломать не планировалось.
Провалившийся тест позволит обнаружить, что в коде сделаны изменения, которые меняют или ломают его поведение. Исследование ошибки, которую выдает провалившийся тест, и сравнение ожидаемого результата с полученным даст возможность понять, где возникла ошибка, будь она в коде или в требованиях.
Отличие от других видов тестов
Все вышесказанное справедливо для любых тестов. Там даже не упомянуты юнит-тесты как таковые. Итак, в чем же их отличие?
Ответ кроется в названии: «юнит» означает, что мы тестируем не всю систему в целом, а небольшие ее части. Мы проводим тестирование с высокой гранулярностью.
17–19 декабря, Онлайн, Беcплатно
Это основное отличие юнит-тестов от системных, когда тестированию подвергается вся система или подсистема, и от интеграционных, которые проверяют взаимодействие между модулями.
Основное преимущество независимого тестирования маленького участка кода состоит в том, что если тест провалится, ошибку будет легко обнаружить и исправить.
И все-таки, что такое юнит?
Часто встречается мнение, что юнит — это класс. Однако это не всегда верно. Например, в C++, где классы не обязательны.
«Юнит» можно определить как маленький, связный участок кода. Это вполне согласуется с основным принципом разработки и часто юнит — это некий класс. Но это также может быть набор функций или несколько маленьких классов, если весь функционал невозможно разместить в одном.
Юнит — это маленький самодостаточный участок кода, реализующий определенное поведение, который часто (но не всегда) является классом.
Это значит, что если вы жестко запрограммируете зависимости от других классов в тестируемый, ошибку, которая вызвала падение теста, будет сложно локализовать, и высокая гранулярность, которая необходима для юнит-тестов, будет потеряна.
Отсутствие сцепления необходимо для написания юнит-тестов.
Другие применения юнит-тестов
Кроме доказательства корректности, у юнит-тестов есть еще несколько применений.
Тесты как документация
Юнит-тесты могут служить в качестве документации к коду. Грамотный набор тестов, который покрывает возможные способы использования, ограничения и потенциальные ошибки, ничуть не хуже специально написанных примеров, и, кроме того, его можно скомпилировать и убедиться в корректности реализации.
Я думаю, что если тесты легко использовать (а их должно быть легко использовать), то другой документации (к примеру, комментариев doxygen) не требуется.
Тем не менее, в этом обсуждении после поста про комментарии видно, что не все разделяют мое мнение на этот счет.
Разработка через тестирование
При разработке через тестирование (test-driven development, TDD) вы сначала пишете тесты, которые проверяют поведение вашего кода. При запуске они, конечно, провалятся (или даже не скомпилируются), поэтому ваша задача — написать код, который проходит эти тесты.
Основа философии разработки через тестирование — вы пишете только тот код, который нужен для прохождения тестов, ничего лишнего. Когда все тесты проходят, код нужно отрефакторить и почистить, а затем приступить к следующему участку.
Об этой технике разработки можно много дискутировать, но ее неоспоримое преимущество в том, что TDD не бывает без тестов, а значит она предостерегает нас от написания жестко сцепленного кода.
И, поскольку TDD предполагает, что нет участков кода, не покрытых тестами, все поведение написанного кода будет документировано.
Возможность лучше разобраться в коде
Когда вы разбираетесь в плохо документированном сложном старом коде, попробуйте написать для него тесты. Это может быть непросто, но достаточно полезно, так как:

