Юнитест что это
Перейти к содержимому

Юнитест что это

  • автор:

Обзор

Пакет программ UniTest System — полноценное решение для создания компьютерных тестов, проведения тестирования (как локально, так и по сети), детального анализа результатов тестирований и составления отчетов на Вашем предприятии или образовательном учреждении.

UniTest System состоит из 5 основных модулей: Editor, Test, Report, Settings и Monitor (сервер и монитор для тестирований по сети). Кроме того, в состав пакета входят UniTest Starter (быстрый запуск) и UniTest Direct (обновления по Интернет).

  • Вопросы теста могут содержать любые OLE-объекты (документы, графика. ), включая интерактивные (анимация, видео и аудио клипы. ).
  • Возможность проигрывания различных процессов, анимации, видео и аудио роликов, и т.п. для каждого вопроса и ответа в тесте. Представьте себе, в качестве вопроса Вы показываете тестируемым презентацию PowerPoint или физический процесс с возможностью изменения параметров его протекания;
  • Встроенный редактор, подобный упрощенному MS Word, позволяет создавать чрезвычайно красивые и наглядные тесты, которые занимают очень мало места (например, полноценный тест из 500 вопросов в среднем занимает 500-700 Kb);
  • В каждом вопросе могут быть как правильные, так и частично правильные (в % от правильного) ответы;
  • Поддержка всех основных и множества дополнительных типов вопросов: множественный выбор, вопросы с различными весами ответов, вопросы на выбор множества из элементов, упорядочивание последовательностей (например, «выберите процессоры по убыванию мощности» или «выберите верные утверждения»), установление соответствия («сопоставьте царей и время их правления»), наличие/отсутствие ключевых слов в тексте ответа, прямой ввод;
  • Возможность «накапливать» базу данных вопросов и выделять разделы в тесте. (Например, у Вас в тесте 3 логических раздела: легкие, средние и сложные задания. База легких вопросов составляет 200 вопросов, средних — 150 и сложных — 170, а при каждом тестировании из базы легких вопросов произвольно выбираются 15 вопросов, из средних — 10 и из сложных — 5 вопросов);
  • Все данные «на лету» шифруются BlowFish 448 бит! (признанный мировой стандарт, используемый, например, Intel для защиты корпоративной информации) и сжимаются в 6-10 раз! Это позволяет практически полностью защитить Ваши тесты от посторонних;
  • Вы можете узнавать всю информацию (выбранный ответ, время, потраченное на ответ, правильность этого ответа и т.д.) о том, как тестируемый проходил любой из вопросов теста;
  • Вы можете создавать собственные профессиональные отчеты по результатам тестирований или использовать уже готовые заготовки;
  • Можно делать отборку записей по любым критериям (например, записи с фамилиями, начинающимися на «К» или оценки не ниже 4) для результатов тестирований и сортировать данные по любым полям.
  • Для каждого элемента любого из вопросов теста можно задавать его размеры и расположение на экране;
  • Для тестирования по сети может использоваться Socket и DCOM-соединение (в том числе и одновременно). Вы можете проводить тестирование даже через Интернет!
  • Мониторинг подключенных пользователей (тестируемых) в реальном времени. Вы немедленно узнаете о любых действиях пользователя (вся информация о пользователе, вплоть до количества набранных баллов и положения в тесте) и сможете на них адекватно среагировать;
  • Администрация подключенных пользователей. Вы можете временно отключать или блокировать особенно назойливых пользователей;
  • Технология сетевой идентификации клиентов и идентификации на уровне операционной системы не позволит кому-либо несанкционированно подключиться к системе и пройти тестирование;
  • Установка и настройка собственной разбалловки по пятибалльной системе;
  • Разбалловка каждого вопроса теста;
  • Полностью настраиваемый интерфейс теста и возможность задавать любые размеры и расположение, устанавливать полноэкранный режим тестирования;
  • Возможность настройки полей ввода данных о тестируемом. Вы можете выбрать, какие поля следует показывать, и добавить свои, полностью настраиваемые поля;
  • Поддержка до 20 вариантов ответов в каждом вопросе;
  • Неограниченное количество вопросов в тесте;
  • Вопросы (в том числе каждый отдельно) и варианты ответов можно перемешивать случайным образом;
  • Режим ограниченного доступа и режим блокировки помогает исключить использование баз данных посторонними;
  • Удобный интерфейс, подобный интерфейсу MS Office XP;
  • Поддержка русского, английского, немецкого, французского, испанского, итальянского, португальского (Португалия и Бразилия), голландского, чешского и болгарского языков.

Editor

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

  • Удобный и легко настраиваемый интерфейс Editor, подобный интерфейсу MS Office XP;
  • Editor может отображать вопросы теста так, как они будут выглядеть при тестировании;
  • Встроенный редактор, подобный упрощенному MS Word, позволяет создавать практически любое содержание для тестов. При этом тесты занимают очень мало места (например, полноценный тест из 500 вопросов в среднем занимает 500-700 Kb);
  • Удобная вставка данных из других программ, работа с буфером обмена;
  • Возможность создания и редактирования любых доступных OLE-объектов прямо в Editor;
  • Удобная навигация и «быстрые клавиши» сэкономят немало времени;
  • Режим ограниченного доступа и режим блокировки помогает исключить использование баз данных посторонними;
  • Режим разметки позволяет расположить элементы вопроса на экране простым перетаскиванием и сжатием/растягиванием этих элементов. Дополнительные возможности: работа с группами элементов, установка относительных размеров и положения.

Test предназначен для проведения тестирования. Для удобства тестирование может проводиться в оконном или полноэкранном режиме. Поля ввода информации о тестируемом можно изменять и добавлять свои. Все элементы Test снабжены всплывающими подсказками. Во время тестирования можно откладывать вопросы (чтобы ответить на них позже). Все результаты тестирования записываются в базу данных. После проведения тестирования в Report можно обработать результаты тестирования и создать на их основе профессиональные отчеты.

  • Все элементы Test имеют всплывающие подсказки, помогающие сориентироваться даже новичку;
  • Полноэкранный режим и продуманное расположение элементов Test на экране помогают отобразить максимум полезной информации с использованием всего доступного пространства;
  • Весь процесс проведения тестирования автоматизирован и не требует вмешательства со стороны;
  • Вопросы (в том числе каждый отдельно) и варианты ответов можно перемешивать случайным образом;
  • Имеется возможность пропуска вопросов (чтобы ответить на них после того, как будут даны ответы на остальные вопросы);
  • Внешний вид Test полностью настраиваем пользователем;
  • Возможность запускать OLE-объекты, проигрывать аудио и видео, и т.п.
  • Результаты тестирования выводятся в баллах и оценкой (по пятибалльной системе);
  • Возможность тестирования, как с помощью мышки, так и с помощью клавиатуры.

Report

Report предназначен для обработки результатов тестирования и создания на их основе профессиональных отчетов. По результатам тестирований можно делать различные выборки и сортировать данные по любым полям. Кроме заранее заготовленных отчетов можно создавать свои собственные отчеты (создание отчетов напоминает создание отчетов в MS Access).

  • Можно делать отборку записей по любым критериям (например, записи с фамилиями, начинающимися на «К» или оценки не ниже 4) для результатов тестирований и сортировать данные по любым полям;
  • Report имеет мощное средство для создания собственных и формирования уже созданных отчетов;
  • Вы можете узнавать всю информацию (выбранный ответ, время, потраченное на ответ, правильность этого ответа и т.д.) о том, как тестируемый проходил любой из вопросов теста;
  • Удобная навигация по базе данных результатов;
  • Возможность предварительного просмотра отчета;
  • Гибкая настройка интерфейса, подобного MS Office XP. Например, можно перетаскивать поля (менять их местами) для более удобной работы с базой данных.

Monitor

Monitor предназначен для администрирования и мониторинга процесса тестирования в сетевой версии пакета. В сетевой версии Test’ы являются клиентами, которые подключаются к Monitor и через него проводят тестирование. Monitor контролирует все действия Test’ов и может разрешать или запрещать подключение отдельных клиентов (Test’ов) или целых групп, отключать или блокировать подключение уже подключенных клиентов. По каждому подключенному клиенту можно посмотреть подробную информацию (статус подключения, информация о тестируемом и о состоянии тестирования на текущий момент).

  • Для тестирования по сети может использоваться Socket и DCOM-соединение (в том числе и одновременно). Вы можете проводить тестирование даже через Интернет!
  • Технология сетевой идентификации клиентов и идентификации на уровне операционной системы не позволит кому-либо несанкционированно подключиться к системе и пройти тестирование;
  • Администрация подключенных пользователей. Вы можете временно отключать или блокировать особенно назойливых пользователей;
  • Возможность мониторинга подключенных пользователей (тестируемых) в реальном времени. Вы немедленно узнаете о любых действиях пользователя (вся информация о пользователе, вплоть до количества набранных баллов и положения в тесте) и сможете на них адекватно среагировать;

Settings

Settings позволяют настроить работу пакета UniTest System в целом и его отдельных компонентов. Можно установить пути к базам данных, используемым по умолчанию, и указать пароли к ним. Можно задать поля ввода данных о тестируемом, которые будут отображаться при тестировании. Здесь же можно настроить разбалловку тестов и внешний вид Test.

UniTest Starter

UniTest Starter занимает минимум места на панели задач и позволяет быстро запускать программы пакета UniTest System: Editor, Test, Report, Monitor, Settings и UniTest Direct.

Зачем нужны юнит-тесты

Обложка: Зачем нужны юнит-тесты

Многие разработчики говорят о юнит-тестах, но не всегда понятно, что они имеют в виду. Иногда неясно, чем они отличаются от других видов тестов, а порой совершенно непонятно их назначение.

Доказательство корректности кода

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

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

Отличие от других видов тестов

Все вышесказанное справедливо для любых тестов. Там даже не упомянуты юнит-тесты как таковые. Итак, в чем же их отличие?

Ответ кроется в названии: «юнит» означает, что мы тестируем не всю систему в целом, а небольшие ее части. Мы проводим тестирование с высокой гранулярностью.

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

Основное преимущество независимого тестирования маленького участка кода состоит в том, что если тест провалится, ошибку будет легко обнаружить и исправить.

И все-таки, что такое юнит?

Часто встречается мнение, что юнит — это класс. Однако это не всегда верно. Например, в C++, где классы не обязательны.

«Юнит» можно определить как маленький, связный участок кода. Это вполне согласуется с основным принципом разработки и часто юнит — это некий класс. Но это также может быть набор функций или несколько маленьких классов, если весь функционал невозможно разместить в одном.

Юнит — это маленький самодостаточный участок кода, реализующий определенное поведение, который часто (но не всегда) является классом.

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

Отсутствие сцепления необходимо для написания юнит-тестов.

Другие применения юнит-тестов

Кроме доказательства корректности, у юнит-тестов есть еще несколько применений.

Тесты как документация

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

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

Тем не менее, в этом обсуждении после поста про комментарии видно, что не все разделяют мое мнение на этот счет.

Разработка через тестирование

При разработке через тестирование (test-driven development, TDD) вы сначала пишете тесты, которые проверяют поведение вашего кода. При запуске они, конечно, провалятся (или даже не скомпилируются), поэтому ваша задача — написать код, который проходит эти тесты.

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

Об этой технике разработки можно много дискутировать, но ее неоспоримое преимущество в том, что TDD не бывает без тестов, а значит она предостерегает нас от написания жестко сцепленного кода.

И, поскольку TDD предполагает, что нет участков кода, не покрытых тестами, все поведение написанного кода будет документировано.

Возможность лучше разобраться в коде

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

Unit-тестирование

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

«IT-специалист с нуля» наш лучший курс для старта в IT

Зачем проводится unit-тестирование

Основной смысл модульного тестирования заключается в том, чтобы избежать накапливания ошибок в будущем, а также исключить регрессию уже отлаженных модулей. Например, у вас есть в целом готовое приложение, к которому необходимо добавить несколько новых функций или процессов. Если сначала выполнить интеграцию компонентов, а потом протестировать полностью «собранное» ПО, то ошибки в дополнениях могут привести к нестабильной работе всего приложения. Чтобы этого не произошло, легче протестировать добавляемые функции изолированно, а после устранения всех багов интегрировать их в программу.

Попробуйте 9 профессий за 2 месяца и выберите подходящую вам

vsrat_7 1 (1)

Таким образом, unit-тестирование решает следующие задачи:

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

Преимущества unit-тестирования

Применять модульное тестирование при разработке программных продуктов рекомендуется по следующим причинам:

  • Простота. Написать тест для отдельного модуля проще, чем для приложения в целом. Соответственно, если нужно проверить не всю программу, а лишь ее часть (например, вышедшее обновление или патч), то можно использовать модульное тестирование, предварительно изолировав проверяемый фрагмент кода. Хотя интеграционное тестирование нужно будет провести в любом случае.
  • Информативность. Хорошо составленный тест помогает разработчикам понять API приложения, функционал модуля, особенности его использования. Особенно это полезно в том случае, если при работе над проектом произошла смена ответственных за разработку и проверку специалистов.
  • Параллельная разработка. Модульное тестирование позволяет проверить работу одного компонента приложения независимо от других. Благодаря этому можно параллельно разрабатывать различные программные модули, тем самым сократив время на создание и отладку продукта.
  • Возможность повторного использования. Создав однажды тест для проверки отдельного модуля, разработчик может вернуться к нему позднее, чтобы протестировать работу компонента еще раз. Регрессионное тестирование состоит в написании контрольных примеров для всех функций, которые помогают выявить ошибки, вызванные внесенными изменениями.

Недостатки unit-тестирования

Несмотря на свои достоинства, модульное тестирование не является панацеей от всех болезней кода:

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

Модульное и интеграционное тестирование

Часто unit-тестирование путают с интеграционным, но это два разных по реализации и назначению уровня проверки программного обеспечения. Отличительные особенности модульного тестирования:

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

Напротив, интеграционное тестирование отличается следующими особенностями:

  • общей направленностью — проверке подвергается не каждый модуль, а вся система, включая основное ядро и функциональные компоненты;
  • сложностью — интеграционное тестирование проводится в среде, максимально близкой к реальной, поэтому требует привлечения внешних ресурсов (баз данных, веб-серверов).

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

Курс для новичков «IT-специалист
с нуля» – разберемся, какая профессия вам подходит, и поможем вам ее освоить

Виды и методы модульного тестирования

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

Автоматизированное. Unit-тестирование заключается в использовании специально разработанных тестовых сред, которые проверяют работу модуля и выявляют в ней ошибки. Такой подход имеет следующие особенности:

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

Методы

«Черного ящика». В этом случае тестирование происходит по входным и выходным сигналам модуля без анализа структуры его кода. Чаще всего такой метод применяется, когда проверку выполняет разработчик, который не участвовал в создании компонента.

«Белого ящика». Суть этого метода в том, что тестируются внутренняя структура модуля, его возможности, особенности поведения, реакция на входные сигналы и т.д. Иными словами, компонент изначально полностью прозрачен и понятен разработчику, который оценивает все внутренние и внешние аспекты его работы.

Для понимания unit-тестирования рассмотрим подробнее, как оно происходит по методу «белого ящика». В этом случае оно состоит из трех этапов:

  • Анализ отдельного модуля. На этой стадии тестирования разработчик изучает внутреннюю структуру кода, функционал и поведение исследуемого компонента. Данный этап пройдет значительно быстрее, если программист сам создавал модуль или участвовал в его создании. Если нет — ему придется поднимать соответствующую документацию, консультироваться с создателем тестируемого фрагмента кода. Главная задача заключается в полном понимании того, как устроен и работает проверяемый программный компонент.
  • Создание кейс-теста. Это сценарий или модель, которые должны показать, как проверяемый модуль ведет себя в реальной обстановке. Кейс-тесты создают искусственную среду, максимально близкую к реальной, но без привлечения внешних ресурсов, которые обычно задействуются в работе программного обеспечения (веб-серверов, баз данных и т.д.).
  • Тестирование модуля. Проверяемый компонент, предварительно изолированный от ядра приложения и других модулей, запускается в кейс-тесте. При этом разработчик смотрит на то, как он реагирует на входные сигналы, как работает сам код, соответствует ли его структура выполняемым задачам, анализирует возможные ошибки и т.д.

Часто к одному и тому же компоненту ПО разработчик применяет различные методики тестирования. Указанные методы «черного и белого ящиков» не исчерпывают всех методик и инструментов проверки. Зачастую разработчик создает под каждый проект уникальные способы тестирования, учитывающие особенности программного продукта.

Разработка через тестирование

Стандартна ситуация, когда разработчик сначала написал код, а затем создает под него тест и выполняет проверку. Но в программировании часто используется и обратный процесс: сначала разрабатывается тест, а модуль создается на его основе. Такой подход называется «разработка через тестирование». Суть его в том, чтобы с помощью заранее написанного теста определить требования к будущему программному компоненту. Цикл разработки через тестирование насчитывает несколько этапов:

  • Добавление теста. Оно происходит перед добавлением каждой новой функции в программу. Написанный тест не запускается по причине того, что проверяемый фрагмент кода еще не написан. Если тестирование сработало — значит, аналогичная или похожая функция в программе уже есть или тест написан некорректно. Сам тест тоже представляет собой программу, поэтому разработчик предварительно должен четко понять, какие результаты она должна показать в случае успешного тестирования.
  • Написание кода. Ориентируясь на то, как должна себя повести тест-программа в «идеальном» случае, разработчик пишет код самого модуля. Причем он не обязан быть сразу совершенным — все неточности будут отшлифованы в последующих циклах разработки. Главное, что требуется от кода, — это прохождение теста. Как только разрабатываемый фрагмент написан, он прогоняется через тест-программу и анализируется.
  • Рефакторинг. Убедившись, что написанный модуль успешно проходит тест, разработчик проверяет его на дублирование, неточности, мусорный код и т.д. Задача на этом этапе — максимально очистить фрагмент, сделать его более прозрачным, простым и понятным.

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

Этот метод разработки имеет свои преимущества:

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

Рекомендации к unit-тестам

Чтобы модульное тестирование было максимально эффективным, тесты должны:

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

Когда не стоит проводить unit-тестирование

Модульное тестирование — не универсальный инструмент проверки программного продукта. В некоторых ситуациях оно лишь отнимет время и силы, не показав значимого результата, например:

  • при тестировании сложных и разветвленных алгоритмов, таких как красно-черное дерево, придется разработать большое число тестов, что существенно усложнит и замедлит проверку;
  • отсутствии четких результатов — например, в математическом моделировании природных процессов, настолько сложных, что их «выход» невозможно спрогнозировать, а можно только описать в виде интервалов вероятных значений;
  • тестировании кода, взаимодействующего с системой, — например, модуля, связанного с портами, таймерами и другими «нестабильными» компонентами, от которых его сложно изолировать;
  • проверке всего приложения — модульное тестирование не покажет ошибки интеграции, баги ядра и другие аспекты, не относящиеся непосредственно к конкретному модулю;
  • недостаточной квалификации самого разработчика и низкой культуре программирования, так как модульное тестирование работает только при строгом соблюдении технологии, постоянном отслеживании всех вносимых в модуль изменениях.

Unit-тестирование окажется бесполезным и при проверке максимально простого кода. Точнее, оно сработает и покажет правильный результат, но сил на написание теста уйдет больше, чем на «ручной» анализ модуля.

Unit-тестирование — это эффективный и полезный инструмент, позволяющий избежать накопления ошибок при разработке программного обеспечения и сильно упрощающий проверку на более высоких уровнях (интеграционную, системную, приемочную).

Наш лучший курс для старта в IT. За 2 месяца вы пробуете себя в девяти разных профессиях: мобильной и веб-разработке, тестировании, аналитике и даже Data Science — выберите подходящую и сразу освойте ее.

Принципы юнит-тестирования. Часть первая

Привет! Меня зовут Владимир, я разработчик команды продукта «Сервис персонализации» в SM Lab. В этом посте я хотел бы рассказать (а в комментариях — обсудить) один очень важный и полезный инструмент разработчика — юнит-тесты.

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

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

Эта статья для всех – кто слышал про них, но не видел, кто приступает к написанию юнит-тестов, и кто их пишет уже давно. Надеюсь, каждый из вас найдет что-то полезное для себя.

При подготовке материала очень помогла книга Владимира Хорикова (@vkhorikov ) «Принципы юнит-тестирования». Рекомендую ее всем, кто хочет еще глубже погрузиться в эту тему.

Что такое юнит-тестирование и для чего оно нужно

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

Зачем мы пишем юнит-тесты?

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

Какими качествами должен обладать юнит-тест

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

По поводу «делать это быстро». В области юнит-тестирования достаточно тяжело определить какие-то границы. Если взять тест, который выполняется за секунду — быстро это или медленно? Всё зависит от того, что это за тест, какие требования к нему. В нашей команде мы опираемся на субъективное восприятие скорости – если, по нашему мнению, тесты проходят быстро – этого достаточно.

Как тесты влияют на разрабатываемые нами продукты

Давайте взглянем на следующий график:

Горизонтальная ось «Прогресс» — своего рода жизненный путь разрабатываемого продукта во времени, где нулевая точка — начало разработки этой системы. В начале жизни продукта развивать его достаточно просто: еще не приняты неудачные архитектурные решения, нет кода, который нужно поддерживать или рефакторить. Поэтому в начале мы можем увидеть, что разработка без тестов, если мы будем сравнивать с другими кривыми, требует минимального времени.

При дальнейшем развитии, когда продукт обрастает кодовой базой и становится сложнее, скорость разработки начинает замедляться, мы тратим больше времени на одинаковый объём доработок. И по графикам видно, что с хорошими тестами увеличение времени достаточно линейно.

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

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

Также хочу предостеречь от попыток тотального покрытия кода тестами. Стоит помнить о том, что тесты так же, как и основной код, имеют свою стоимость. Это можно увидеть на предыдущем графике – разность по вертикальной оси между началами графиков – без тестов и с тестами. Эта дельта времени и есть стоимость тестов на старте. Тесты требуют как разработки, так и поддержки. Плюс к этому, чем больше кода написано, тем больше потенциальных ошибок там может быть. На тесты мы не пишем тесты, поэтому большой объём тестового кода может содержать ошибки. Также увеличивается время прохождения тестов и уменьшается желание их часто запускать.

Как измерить покрытие кода тестами

Для этого есть две чаще всего используемые метрики – процент покрытия строк (code coverage) и процент покрытия логических ветвей (branch coverage) кода.

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

Но даже стопроцентное покрытие тестами не гарантирует хорошего качества тестов. И вот почему. Эти метрики не отражают некоторые важные детали, такие как наличие и полноту проверок (asserts). Мы можем написать тест, который не имеет проверок вообще. То есть он будет запускать наш основной код, но по факту не будет иметь никакой ценности. И ещё, эти метрики не оценивают код во внешних библиотеках. Они оценивает только тот код, который написали мы.

Вот короткий пример:

Есть метод, который принимает строку и превращает её в число. Очень простой метод. На него написан простой тест, который передаёт строку со значением «5» и сравнивает число 5 с тем, какое число возвращает метод.

Если мы посмотрим на этот тест и попробуем оценить метрики покрытия, мы увидим, что для этих метрик покрытие будет 100%. Но, к сожалению, если мы заглянем в библиотечный метод parseInt, мы увидим, что на самом деле мы проверили только один из четырех вариантов. Поэтому с одной стороны у нас покрытие 100%, но с другой стороны мы покрыли по факту только 25% кода. Поэтому на эти метрики стоит ориентироваться, но не слепо им доверять. В нашей команде мы ориентируемся на уровень в 80% покрытия кода.

Тестовые двойники

Помощниками в тестировании выступают «тестовые двойники» (test doubles). Их выделяют 5 видов.

Пустышка (Dummy). Такой двойник не содержит поведения и используется в качестве заполнителя параметров. Никогда реально не вызывается.

Подделка (Fake). Это реально написанная реализация, которая имеет более простую логику, чем реальный аналог.

Есть ещё три вида похожих между собой тестовых двойников. Но есть и различия.

Заглушка (Stub) — имеет заранее подготовленные ответы на вызовы методов. Практически не имеет логики.

Шпион (Spy). Более сложная система. Это гибрид реального объекта и мока. Он имеет поведение реального объекта, но может записывать определенную информацию о вызове его методов. Также можно переопределить поведение некоторых методов.

Мок (Mock). Самый сложный из тестовых двойников, но дающий наибольший контроль. Может иметь сложную логику ответов, зависящих от параметров вызовов, их количества, генерировать исключения при вызове методов с неопределенным поведением и имеет другой, полезный для тестирования функционал.

Все тестовые двойники, за исключением fake, в основном создаются с помощью фреймворков, с которыми мы часто работаем. Для Java это Mockito, для Kotlin – MockK, для других языков такие фреймворки тоже есть.

Изоляция и виды зависимостей

Что такое зависимость? Класс чаще всего не существует изолированно в коде. Он использует какие-то другие части программы. Зависимости – это то, что использует класс для своей работы. Это могут быть другие классы, базы данных, файловые системы, сторонние сервисы и прочее.

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

Совместные зависимости (shared). Через такие зависимости тесты могут влиять на результаты друг друга. Например, статические изменяемые поля класса, база данных. Это изменяемые зависимости.

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

Приватные зависимости (private). Это зависимости, не являющиеся совместными. Они могут быть как изменяемыми, так и неизменяемыми. Та же база данных может быть как совместной, так и приватной зависимостью. Совместной она может выступать, когда все тесты работают с одной базой. Приватной зависимостью база данных может выступать в случае, когда для каждого теста поднимается отдельная база данных, например в docker-контейнере. В этом случае тесты не смогут повлиять друг на друга через базу.

Есть два подхода (школы) к пониманию изоляции: так называемые классическая и лондонская школы.

Лондонская школа понимает изоляцию тестируемого кода как изоляцию от его изменяемых зависимостей. Все изменяемые зависимости (совместные и приватные) заменяются на тестовые двойники – «мокируются».

В этом подходе есть следующие плюсы:

можно разбить взаимосвязанный граф классов на отдельные классы, заменив изменяемые зависимости тестовыми двойниками, и тестировать их независимо.

позволяет однозначно определить, что юнит – это класс.

в случае падения теста поиск проблемы ограничивается одним классом.

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

Мы в команде чаще всего используем этот подход.

Классическая школа подразумевается изоляция не тестируемого кода, а тестов друг от друга. Если тесты через зависимость могут влиять друг на друга при параллельном запуске, то эта зависимость меняется на тестовый двойник. Все остальные зависимости, даже если они изменяемые, используются как есть. То есть при таком подходе зачастую тестируется одновременно несколько связанных классов.

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

При таком подходе юнитом является не класс, как в Лондонской школе, а единица поведения, и она часто не ограничивается одним классом. При изменении одного из классов падают тесты и всех зависимых классов. Иногда тяжело понять причину массового падения тестов. Для упрощения отладки стоит чаще запускать юнит-тесты, что поможет понять, какие изменения в коде повлияли на тесты и локализуют поиск. Это также позволяет понять, какие классы наиболее важные и на какие нужно обратить особое внимание.

Эффективность юнит-тестов

Чтобы формально как-то измерить эффективность наших тестов, мы можем представить ее как произведение четырёх параметров.

защита от багов – возможность выявить ошибку, которая зависит от объема выполняемого кода, его сложности и важности.

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

быстрая обратная связь – быстро выполняемые тесты ускоряют обратную связь.

простота поддержки – насколько сложно понять и запустить тест.

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

Давайте рассмотрим следующие отношения между поведением тестируемого кода и теста:

В случаях, когда функциональность работает правильно и тест проходит (отрицательное срабатывание) или функциональность работает неправильно и тест не проходит (истинное срабатывание) – это ожидаемое поведение теста. В этом случае можно считать, что тест работает правильно.

Есть ещё и две другие области. Ложное срабатывание — функциональность работает правильно, но тест не проходит. Это значит, что тесты, имеют низкую устойчивость к рефакторингу. Чаще всего это означает, что тесты завязаны на детали имплементации и будут падать при изменении реализации тестируемого кода без изменения функциональности. Низкая защита от багов – случай, когда тест проходит, но функциональность работает неправильно.

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

Сквозные тесты (e2e-тесты) задействуют большой объём кода, проходя через цепочку тестируемых элементов или систем, поэтому имеют хорошую защиту от багов. Такие тесты устойчивы к рефакторингу, так как ориентируются на внешний интерфейс – API, и ничего не знают про детали реализации. Такие тесты достаточно медленные — и с точки зрения написания, и с точки зрения прохождения.

Тривиальные тесты могут быть устойчивы к рефакторингу и иметь быструю обратную связь. Например тест, написанный на getter – метод получения значения приватного поля. Такой тест переживет рефакторинг getter-а (если такой вообще будет) и будет быстро проходить. Но пользы от такого теста не будет.

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

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

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

проще и быстрее они должны разрабатываться.

ниже затраты на поддержку тестов.

быстрее скорость прохождения отдельного теста.

Перечисленные факторы влияют на выбор между устойчивостью к багам и быстрой обратной связью.

Устойчивость к рефакторингу желательно держать максимальной, поэтому что это величина достаточно бинарная – тесты либо устойчивы к рефакторингу, либо нет. Защита от багов и быстрая обратная связь – более эластичны, поэтому выбор идет между ними.

Сквозных тестов меньше всего – они довольно медленные и не всегда отличаются простотой в поддержке. Но они задействуют большой объем кода и покрывают в основном критичную функциональность, поэтому у них высокая защита от багов.

Юнит-тестов в основном самое большое количество, поэтому для них важна скорость выполнения.

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

На этом первая часть закончена. В следующей части будет рассмотрена структура юнит-теста, поделюсь тем, какие подходы используются у нас в команде. Расскажу про стили юнит-тестирования, принципы рефакторинга для эффективных юнит-тестов, рассмотрю некоторые антипаттерны при написании тестов.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *