Готовый перевод The Art of Unit Testing / Искусство модульного тестирования: Part 1. Getting started
Часть 1. Приступая к работе
В этом разделе книги описываются основы модульного тестирования.
В первой главе я расскажу что такое модуль и что понимается под "хорошими" подходами к модульному тестированию, а также я проведу сравнение между модульным и интеграционным тестированием. Затем мы рассмотрим разработку через тестирование и ее роль в отношении модульного тестирования.
Вы будете пробовать писать ваш первый модульный тест, используя NUnit во второй главе. Вы познакомитесь с основами NUnit API, его применением, и научитесь запускать тест в среде NUnit.
Глава 1. Основы модульного тестирования
Содержание главы:
Определение юнит-теста
Сравнение модульного и интеграционного тестирования
Рассмотрение простого примера юнит-теста
Понимание разработки через тестирование
Всегда есть первый шаг: Ваша первая программа, первый провальный проект, первая успешная попытка. Вы никогда не забудете свой первый раз, и я надеюсь, что вы не забудете свои первые тесты. Возможно, вы уже написали несколько тестов, и даже можете вспомнить их - такие плохенькие, неудобные, медленные или несерьезные (большинство делают такие). Больше оптимизма ?! Возможно, у вас был отличный первый опыт работы с юнит-тестами, и возможно, вы можете пропустить больше.
В этой главе мы сначала рассмотрим "классическое" определение юнит-теста и сравним его с концепцией интеграционного тестирования. Это различие - заблуждение многих. Затем мы рассмотрим плюсы и минусы модульного тестирования в сравнении с интеграционным тестированием и дадим более четкое определение «хорошего» модульного тестирования. Закончим главу рассмотрением вопроса разработки через тестирование, потому что, это зачастую связано с модульным тестированием. В тексте главы я также затрону понятия, которые более тщательно будут объясняются в других главах книги.
Давайте начнем с определения, что такое - модульные тесты.
1.1. Определение модульного тестирования - шаг за шагом
Модульное тестирование - не новая концепция в программировании. Оно появилось в первые дни программирования на Smalltalk в 70-х. Снова и снова оно показывает себя одним из лучших способов, с помощью которых разработчик может улучшить качество кода имея более глубокое понимание функциональных требований класса или метода.
Кент Бек ввел понятие модульного тестирования в Smalltalk и позже распространил его на другие языки программирования, сделав модульное тестирование - одной из самых полезных практик в разработке ПО. Прежде, чем двинуться дальше, нам нужно дать более четкое определение модульному тестированию. Ниже приведено классическое определение, из Википедии. Мы будем постепенно развивать и дополнять его в этой главе. Окончательный вариант определения появится в разделе 1,4,
Определение 1.0
Юнит-тест - это фрагмент кода (обычно, отдельный метод) который вызывает исполнение другого фрагмента кода и после этого проверяет правильность некоторых предположений. Если предположения не подтверждаются, считается, что выполнение модульного теста закончилось неудачей. Тестируемый блок - метод или функция.
Вещь, для которой пишутся модульные тесты, называется "тестируемая система" (SUT - system under test).
Определение
SUT расшифровывается как "system under test" - тестируемая система. Некоторые люди используют сокращение CUT - "class under test" (тестируемый класс) или "code under test" (тестируемый код). Когда вы тестируете что-то, SUT - это то, что вы тестируете.
Раньше я чувствовал (да, чувствовал - в этой книге нет науки, только искусство), что это определение модульного теста технически корректно, но за последние пару лет, мое понимание идеи модульных тестов сильно изменилось. Для меня блок является "единицей работы" или "прецедентом" ("user case") внутри системы.
Единица работы - это совокупность действий от вызова публичной функции до получение единственного значимого результата, который проверяется. Значимый результат может быть проверен без учета внутреннего состояния системы и только через публичные API системы и её поведение. Результат должен быть одним из:
Вызываемый публичный метод возвращает значенначение (не void).
Уведомление о смене внутреннего состояния или поведения системы, который определяется без просмотра внутреннего состояния. (Пример: система записывает в лог попытку авторизации или смену состояние системы, если это конечный автомат.)
Вызов внешних систем, которые вы не контролируете. Эти внешние системы не игнарируют ваш запрос и возвращают не случайное значение. (Пример: вызов внешней системы логирования, которую вы не писали и у вас нет исходников этой системы).
Единица работа может быть как вызов конкретной функции, так и нескольких классов, объединенный по их назначению.
Вы можете захотеть минимизировать единицу работы для тестов. Я так делал. Больше не делаю. Я верю, что можно создать такой большой модульный тест, что его результат будет более информативен пользователю вашей публичной API, такие тесты легче поддерживать. Если вы попытаетесь минимизировать единицу работы, вы сломаете себе мозг при разборе причин неизвестного результата публичной API, вместо того чтобы двигаться дальше. Эта тема будет развита сильнее, позже (в основном в 8 главе).
Обновлённое определение 1.1
Модульный тест - это кусок кода который запускает единицу работы и проверяет один специфичный результат данной работы. Если предполагаемый результат не совпадает с результатом вызова, то тест провален. Модульный тест может быть как отдельной функции так и совокупности классов.
Не важно какой язык программирования вы используете, самое главное в модульных тестах - определить что такое "хорошо".
1.1.1. О важности написания хороших модульных тестов
Понимание единицы работы не достаточно.
Многие, кто пытался покрыть код тестами или переставали использовать тесты или не использовать вовсе. Вместо этого они надеялись что тесты пригодятся позднее или они в ручную тестировали код с использованием сторонних приложений или используя конечный продукт для проверки своего кода.
Нет смысла писать плохой модульный тест, если вы не научиться писать хороший и это ваши первые шаги в этой области. Если вы собираетесь написать плохой модульный тест, лучше не пишите его, этим вы сэкономите уйму времени разработки. Ваша цель - убедиться что у вас хороший тест, а не плохой.
Чтобы понять что такое хороший модульный тест - давайте посмотрим, что програмисты делают когда хотят проверить что-либо.
Как вы можете убедиться что ваш код сегодня все еще работоспособен?
1.1.2. Мы все пишем модульные тесты (вроде)
Вы можете быть удивлены услышав такое, но на самом деле вы применяете некоторые виды модульных тестов в своей работе. Вы когда нибудь общались с разработчиком который не проверяет свой код перед использованием, кроме меня?
Вы можете использовать консольное приложение для проверки различных методов классов или компонентов, или создать специальный (WinForm, Web Form) пользовательский интерфейс, для проверки функциональности класса или компонента, или может быть каждый раз в ручную выполнять множество действия в реальном интерфейсе приложения. Конечный результат - код работает как надо или что-то еще.
Рисунок 1.1. показывает, как большинство разработчиков проверяют свой код. Пользовательский интерфейс (UI) может меняться, но не меняется шаблон: ручной запуск внешнего инструмента для проверки чего либо снова или запуск приложения и проверка в ручную.
Рисунок 1.1. В классическом тестировании, разработчик использует графический пользовательский интерфейс (GUI) для привязки действия к классу которой он проверяется. Потом он сравнивает результат.
Эти тесты вполне пригодны и они больше подходят к классическому определению модульного теста, но они сильно отличаются от того что я называю хорошим модульным тестом в этой книге. Самый первый и главный вопрос, для разработчика, определить критерии хорошего модульного тесте: что такое модульный тест и что не модульный тест?
1.2. Критерии хорошего модульного теста
Модульный тест должен отвечать следующим критериям:
Тест должен быть автоматизирован и воспроизводим.
Тест должен быть прост в применении.
Тест не должен терять актуальность в будущем.
У любого человека должна быть возможность запустить тест одним нажатием кнопки.
Выполнение теста не должно занимать много времнеи.
Результаты теста должны быть постоянными (всегда должны возвращаться одни и те же результаты, если между прогонами теста ничего не менялось).
Тест должен иметь полный контроль над тестируемым модулем.
Тест должен быть полностью изолированным (выполняться независимо от других тестов).
Когда тест падает, должна быть возможность легко определить, какой результат ожидался и в чем именно заключается проблема.
Много людей путают сам акт тестирования их программного обеспечения с концепцией юнит теста. Прежде чем начать, задайте себе следующие вопросы о тестах которые вы написали до этого момента:
Могу ли я запустить и получить результаты юнит-теста, который я написал две недели (или месяцы или годы) назад?
Может ли любой член моей команды запустить и получить результаты теста, написанного мной два месяца назад?
Могу ли я запустить все тесты, которые я написал, и получить их результаты не более, чем за несколько минут?
Могу ли я запустить все модульные тесты нажатием одной кнопки?
Могу ли я написать базовый тест не более, чем за несколько минут?
Если вы ответили "нет" на любой из этих вопросов, есть большая вероятность, что то, что вы реализовали, не является модульными тестами. Это определенно какие-то тесты и они так же важны, как и модульные тесты, но у них есть недостатки, по сравнению с тестами, позволяющими ответить "да" на все вопросы.
"Так что же я делал до сих пор?", возможно, спросите вы. Вы занимались интеграционным тестированием.
1.3. Интеграционные тесты
Я считаю, интеграционные тесты - это любые тесты, которые выполняются не быстро, не имеют постоянного результата или используют реальные зависимости тестируемых блоков. Например, если тест использует реальное системное время, реальную файловую систему или реальную БД, он вошел в царство интеграционного тестирования.
Если тест зависит от системного времени (например, в тестовом коде используется DateTime.Now), то каждый раз, когда тест выполняется, это, по сути, другой тест, т.к. он использует другое время. Этот тест уже не является постоянным (его результат при запуске в одинаковых условиях может отличаться).
Сами по себе интеграционные тесты - это не плохо. Я думаю, интеграционные тесты являются важными партнерами тестирования, но они должны быть отделены от модульных тестов, чтобы добиться ощущения «безопасной зеленой зоны", которая обсуждается в этой книге.
Если тест использует реальную БД, он больше не выполняется целиком в памяти и его данными сложнее оперировать, чем тестовыми данными в памяти. Такой тест также может выполняться дольше и мы не сможем это контролировать. Модульные тесты должны быть быстрыми. Интеграционные тесты, как правило, гораздо медленнее. Когда вы запускаете одновременно сотни тестов, каждые полсекунды имеют значение.
Интеграционные тесты также увеличивают риск еще одной проблемы: тестирование слишком многих вещей одновременно.
Что происходит, когда ваша машина ломается? Как вы узнаете, в чем проблема, не говоря уже о том, как ее исправить? Двигатель состоит из многих подсистем, работающих вместе. Каждая из них взаимодействует с другими, чтобы помочь получить окончательный результат: движение автомобиля. Если автомобиль перестает двигаться, то неисправность может быть в любой из этих подсистем - или более, чем в одной. Интеграция этих подсистем (или слоев) - это то, что заставляет автомобиль двигаться. Вы можете думать о движении автомобиля как о критерии корректной работы всех его частей. Если тест не пройден, все части не работают вместе; если тест успешен - все части работают правильно.
То же самое происходит в программном обеспечении. Большинство разработчиков теситруют реализованный функционал при помощи UI. Нажатие некоторой кнопки вызывает ряд событий классов и компонентов, работающих вместе для получения окончательного результата. Если тест не пройден, все эти программные компоненты - не работают. И может быть трудно понять, что стало причиной провала всей операции (см. рисунок 1.2).
Рисунок 1.2. В интеграционном тесте у вас может быть несколько точек отказа. Все модули должны работать вместе, и любой из них может отказать, делая затруднительным поиск источника ошибки.
Как определено в "Полном руководстве по тестированию ПО" Билла Хецела (Wiley, 1993), интеграционное тестирование - это "организованное тестирование, в котором ПО и/или аппаратная часть объединены и тестируются, когда вся система собрана." Это определение интеграционного тестирования почти не отличается от того, что многие люди делают все время, считая это не тестированием системной интеграции, а частью процесса разработки и модульного тестирования.
Далее идет более ясное определение интеграционного тестирования
Определение
Интеграционное тестирование - это тестирование модуля без полного контроля над процессом и с использованием одной или многих его зависимостей от "реального мира" (например, таких как время, сеть, БД, потоки, генераторы случайных чисел и т.п.).
Резюмируя: все интеграционные тесты используют реальные зависимости; модульные тесты изолируют модуль от его зависимостей, чтобы иметь возможность легко получать однозначный результат, а также легко контролировать и симулировать любой аспект поведения модуля.
Вопросы из раздела 1.2 могут помочь вам понять некоторые недостатки интеграционного тестирования. Давайте попробуем определить качества модульных тестов, которые нас интересуют.
1.3.1 Недостатки ручных интеграционных тестов в сравнении с автоматизированными модульными тестами.
Давайте применим вопросы раздела 1.2 к интеграционным тестам и рассмотрим цели, которые мы хотим достичь с помощью модульных тестов в реальном мире.
Можете ли вы запустить и получить результаты из теста, который написан две недели назад (или месяцы/годы назад)? Если нет, то как вы можете узнать, не сломался ли функционал, написанный ранее? Во время разработки код постоянно меняется и если вы не можете (или не хотите) запускать тесты для всех ранее работающий функций, во время очередного изменения кода вы можете сломать их и не узнаете об этом. Я называю это "случайное деланье ошибок" (“accidental bugging”) и это обычно происходит ближе к концу программного проекта, когда разработчики находятся под давлением, чтобы исправить существующие ошибки. Иногда они непреднамеренно делают новые ошибки в процессе исправления старых. Можно ли легко понять, что вы что-то сломали в течение трех минут после поломки? Далее в этой книге вы увидите, как это можно сделать.
Определение
Регрессия - это ситуация, когда один или несколько блоков раньше работали, а сейчас - не работают.
Может ли любой член вашей команды запустить и получить результаты из тестов, написанных вами месяц назад? Этот вопрос вытекает из предыдущего и ставит его на ступеньку выше. Вы хотите быть уверены, что ваши изменения не сломали чужой код. Многие разработчики боятся менять унаследованный код в старых системах, т.к. не знают какие места системы зависят от изменяемого фрагмента кода. По сути, это риск перевести систему в неизвестное состояние в плане стабильности. Есть очень мало вещей, более страшных, чем неизвестность, работает ли еще приложение (особенно, если код писали не вы). Если бы вы точно знали, что ничего не сломали, вы значительно меньше боялись бы изменять незнакомые фрагменты кода (безопасность гарантировалась бы сетью модульных тестов). Если модульный тест написан хорошо, то любой разработчик может запустить его.
Определение
Согласно Википедии, унаследованный код - это "исходный код, который больше не поддерживается производителем программного продукта", но многие считают любую старую версию приложения (в т.ч. поддержка которых еще не прекращена) унаследованным кодом. Это часто относится к коду, с которым трудно работать, который трудно проверить, и, как правило, даже трудно читать.
Однажды клиент определил унаследованный код логичным образом как "код который работает". Много людей предпочитают определять унаследованный код как "код не имеющий ни одного теста". Эффективная работа с унаследованным кодом Майкла Физерса (Prentice Hall, 2004) использует это определение в качестве официального, и это определение мы будем рассматривать, читая эту книгу.
Я могу запустить написанные тесты не более чем за пару минут? Если вы не можете запустить тесты быстро (лучше за секунды чем за минуты), вы будете запускать их не так часто (ежедневно, еженедельно или даже ежемесячно в некоторых местах). Проблема в том, что когда вы меняете код, вы хотите сразу же видеть результат не сломали ли вы что-нибудь.Чем больше требуется времени на выполнение тестов, тем больше изменений вы вносите в систету и к тому моменту, когда вы поймете, что что-то сломали, у вас будет (на много) больше мест с возможными ошибками.
Я могу запустить написанные тесты нажатием одной кнопки? Если нет, это говорит или о том, что вам возможно придется настраивать устройство для правильного прогона теста (установка строки подключения к базе данных, например), или о том, что модульные тесты не автоматизированы полностью. Если вы не можете полностью автоматизировать тесты, вы скорее всего не будете запускать их в повторяющемся режиме, как не будут и другие члены вашей команды. Никто не хочет застревать с настройками для запуска теста только чтобы проверить работает ли еще система. Для разработчиков важнее добавление нового функционала в систему. Хорошие тесты должны легко выполняться в исходном виде, а не вручную.
Я могу написать базовый тест не более чем за пару минут? Одним из самых очевидных признаков интеграционного теста является то, что тест требует время на правильную подготовку и имплементацию, а не только на выполнение. Внутренние и, иногда, внешние зависимости увеличивают время на то, чтобы выяснить, как написать тест. (Базы данных можно воспринимать как внешние зависимости). Если вы не автоматизируете тест, то зависимости это меньшая часть проблемы, но в результате вы не получите преимуществ автоматизированных тестов. Чем сложнее написать тест, тем меньшее количество тестов вы напишите, и тем меньше времени, чтобы сосредоточиться на других частях кода, помимо "самого большого" участка, которым вы сейчас всецело поглощены. Одной из сильных сторон модульного тестов является то, что они, как правило, выявляют не только баги в самых важных частях программы, но и все мелкие ошибки в других местах, способные сломать код. Очень часто программисты удивляются, как много ещё можно найти ошибок в простом и, на их взгляд, чистом коде. Если вы сосредотачиваетесь только на больших тестах, то покрытие логики тестами будет меньшим. Когда часть основной логики не покрыта юнит-тестами (даже в случае покрытия большинства компонентов), возможно существование множества таких ошибок, о которых вы и не предполагали. Как только вы определите закономерности вашей конкретной тестируемой объектной модели, написание хороших юнит-тестов станет простым и быстрым занятием. Небольшое замечание: даже опытные юнит-тестеры находят, что написание первого теста для новой объектной модели, которую они видят впервые, может занять 30 минут и более. Это часть работы и это нужная и важная часть. Второй и последующие тесты, после проделанной предварительной работы, написать уже гораздо проще.
Что делает модульные тесты "хорошими"?
Листинг 2.5. Тестирование класса через вызов метода и проверка значения свойства.
[TestCase("badfile.foo",false)]
[TestCase("goodfile.slf",true)]
public IsValidFileName_WhenCalled_ChangesWasLastFileNameValid(string file, bool expected)
{
LogAnalyzer la=MakeAnalyzer();
la.IsValidLogFileName(file);
Assert.AreEqual(expected,la.WasLastFileNameValid);
}
Следующий листинг покажет другой пример. Заглянем как работает карманный калькулятор.
Листинг 2.6 Методы Add() и Sum()
public class MemCalculator
{
private int sum=0;
public void Add(int number)
{
sum+=number;
}
public int Sum()
{
int temp=sum;
sum=0;
return temp;
}
}
Класс MemCalculator работает так же как корманный калькулятор, к которому вы привыкли. Вы жмете на число, затем плюс, затем другое число,снова плюс, и так далее. Когда вы закончили, вы можете нажать на равно и получить результат.
С чего начать тестирование метода Sum()? Вам следует начать с простого теста, к примеру возвращение 0 по умолчанию. Этот тест показан в следующем листинге.
Листинг 2.7. Простой тест подсчета суммы для калькулятора
Обратите внимания как называется метод. Читается как приговор.
Вот простой перечень именования сценариев, который мне нравится использовать в таких случаях:
ByDefault (по умолчанию) может быть использован, где результат не зависит от действия (как в прошлом примере).
WhenCalled (когда вызывается) или Always (всегда) - может быть использован для второго или третьего варианта результата (изменение состояния или вызов внешних источников), когда состояние изменилось и нет приоритетного результата или когда внешний источник прислал ответ, но нет приоритета результата. Для примера Sum_WhenCalled_CallsTheLogger (Сумма когда вызывается внешний Logger) или Sum_Always_CallsTheLogger (Сумма всегда для вызова внешнего Logger).
Вы можете написать другие варианты тестов, без первого вызова метода Add(), но следующие тесты будут использовать метод Add() и сравниваться с результатом вызова Sum(). Следующий листинг тестирует класс новым тестом.
Листинг 2.8. Два теста, второй тест вызывает метод Add() первого.
Отлично, Но что произойдет если метод будет использовать внешние ресурсы (файловая системе, база данных, веб сервис), которые вы не контролируете? И как вы проверите другой тип результата единицы работы - вызывая внешнее приложение? С этого момента вы начнете создавать тестовые заглушки (Stub), поддельные объекты, макеты объектов (Mock), обсудим в следующей главе.
2.8. Заключение
В этой главе мы рассмотрели использование NUnit для написания простого теста, против простого кода. Вы использовать атрибуты [TestCase],[SetUp] и [TearDown] для того чтобы убедиться, что тесты всегда используют новое и не измененное состояние объекта. Вы использовали шаблон Фабричный метод, для того чтобы сделать код более обслуживаемым. Вы использовали [Ignore] для того чтобы исключить тест из проверки. Категории тестов позволят вам группировать тесты логичнее, чем имена классов и пространства имен, и Assert.Catch() поможет вам быть уверенными что ваш код вызывает исключение, когда это надо. Мы так же рассмотрели что происходит когда вы используете простой метод с возвращаемым значением, когда вам надо проверять состояние объекта.
Этого не достаточно, хотя. Сам тестирующий код, может быть сложнее чем тестируемый код. Следующая пара глав даст вам дополнительные базовые инструменты для написания модульных тестов. Вам предстоит выбирать инструмент, когда вы столкнетесь с более сложным сценарием тестирования.
В заключение, помните о:
В основном, используется один класс-тест для тестируемого класса, один проект модульных тестов, для тестируемого проекта (+ проект для интеграционного тестирования), и по крайней мере один метод испытания на единицу работы (которые могут быть как на конкретный метод, так и несколько классов).
Именуйте свои тесты понятно, к примеру по такому шаблону: [ЕдиницаРаботы]_[Сценарий]_[Ожидаемое_поведение].
Используйте шаблон Фабричный метод, для повторного использования тестов, например для создания и инициализации объектов используемых в тестах.
Не используйте [SetUp] и [TearDown] если можете обойтись без них. Данные атрибуты делают тесты менее понятными.
В следующей главе, мы взглянем на несколько реальных примеров, где код тестируются более реалистично чем вы видели до сих пор У него будут зависимости и проблемные места и мы начнем обсуждение что такое подделка, моки и стабы, и как вы можете их использовать при написании тестов против такого кода..
Внимание! Этот перевод, возможно, ещё не готов.
Его статус: идёт перевод
Переведено на Нотабеноиде
http://notabenoid.org/book/50563/210882
Переводчики: nekalex, dima117, SakhipovRR, ChiFuka
novmicha, Anthrax, Alena88, nik_vik
dolph
Настройки
Готово:
