Виды тестирования и подходы к их применению

Из институтского курса по технологиям программирования я вынес следующую классификацию видов тестирования (критерий — степень изолированности кода). Тестирование бывает:
  • Блочное (Unit testing) — тестирование одного модуля в изоляции.
  • Интеграционное (Integration Testing) — тестирование группы взаимодействующих модулей.
  • Системное (System Testing) — тестирование системы в целом.
Классификация хорошая и понятная. Однако на практике выясняется, что у каждого вида тестирования есть свои особенности. И если их не учитывать, тестирование станивится обременительным и им не занимаются в должной мере. Здесь я собрал подходы к реальному применению различных видов тестирования. А поскольку я пишу на .NET, ссылки будут на соответствующие библиотеки.

Блочное тестирование


Блочное (модульное, unit testing) тестирование наиболее понятное для программиста. Фактически это тестирование методов какого-то класса программы в изоляции от остальной программы.

Не всякий класс легко покрыть unit тестами. При проектировании нужно учитывать возможность тестируемости и зависимости класса делать явными. Чтобы гарантировать тестируемость можно применять TDD методологию, которая предписывает сначала писать тест, а потом код реализации тестируемого метода. Тогда архитектура получается тестируемой. Распутывание зависимостей можно осуществить с помощью Dependency Injection. Тогда каждой зависимости явно сопоставляется интерфейс и явно определяется как инжектируется зависимость — в конструктор, в свойство или в метод.

Для осуществления unit тестирования существуют специальные фреймворки. Например, NUnit или тестовый фреймфорк из Visual Studio 2008. Для возможности тестирования классов в изоляции существуют специальные Mock фреймворки. Например, Rhino Mocks. Они позволяют по интерфейсам автоматически создавать заглушки для классов-зависимостей, задавая у них требуемое поведение.

По unit тестированию написано много статей. Мне очень нравится MSDN статья Write Maintainable Unit Tests That Will Save You Time And Tears, в которой хорошо и понятно рассказывается как создавать тесты, поддерживать которые со временем не становится обременительно.

Интеграционное тестирование


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

Если к нему подходить как к unit-тестированию, у которого в тестах зависимости не заменяются mock-объектами, то получаем проблемы. Для хорошего покрытия нужно написать много тестов, так как количество возможных сочетаний взаимодействующих компонент — это полиномиальная зависимость. Кроме того, unit-тесты тестируют как именно осуществляется взаимодействие (см. тестирование методом белого ящика). Из-за этого после рефакторинга, когда какое-то взаимодействие оказалось выделенным в новый класс, тесты рушатся. Нужно применять менее инвазивный метод.

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

Хорошая статья по интеграционному тестированию мне попалась лишь однажды — Scenario Driven Tests. Прочтя ее и книгу Ayende по DSL DSLs in Boo, Domain-Specific Languages in .NET у меня появилась идея как все-таки устроить интеграционное тестирование.

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

1) Допустим в присланных документах есть несколько разделов. Тогда в спецификации мы можем указать, что у разбираемого документа должны быть разделы с указанными именами:

$SectionNames = Введение, Текст статьи, Заключение, Литература

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

$IsCoverable = true
Понятно, что для проверки подобных спецификаций потребуется движок, который бы считывал спецификации и проверял их соответствие поведению программы. Я такой движок написал и остался доволен данным подходом. Скоро выложу движок в Open Source. (UPD: Выложил)

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

Системное тестирование


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

Первый подход — это использовать вариацию MVC паттерна — Passive View (вот еще хорошая статья по вариациям MVC паттерна) и формализовать взаимодействие пользователя с GUI в коде. Тогда системное тестирование сводится к тестированию Presenter классов, а также логики переходов между View. Но тут есть нюанс. Если тестировать Presenter классы в контексте системного тестирования, то необходимо как можно меньше зависимостей подменять mock объектами. И тут появляется проблема инициализации и приведения программы в нужное для начала тестирования состояние. В упомянутой выше статье Scenario Driven Tests об этом говорится подробнее.

Второй подход — использовать специальные инструменты для записи действий пользователя. То есть в итоге запускается сама программа, но щелканье по кнопкам осуществляется автоматически. Для .NET примером такого инструмента является White библиотека. Поддерживаются WinForms, WPF и еще несколько GUI платформ. Правило такое — на каждый use case пишется по скрипту, который описывает действия пользователя. Если все use case покрыты и тесты проходят, то можно сдавать систему заказчику. Акт сдачи-приемки должен подписать.
+41
19 января 2010, 15:15
95

комментарии (12)

+1
FlashXL #
Почему в блоге .NET? Помоему описанные принципы справделивы для любого языка. Я бы запостил в Тестирование
0
SychevIgor #
Поддерживаю перенос в Тестирование. У автора нету ни строчки про .Net конкретно. Конечно в качестве примеров есть упоменания специфичных технологий, но ради пары упомянаний нет смысла. Все что автор сказал можно в общем отнести именно к общим моментам тестирования.
0
FallenGameR #
Перенес.
0
LunarFrog #
По поводу движка для интеграционного тестирования – идея не новая, мы давно и успешно используем Fitnesse.
Для описания Fit-теста используется HTML документ, который вполне может создать тестировщик и передать программисту для использования. В документе описываются входящие данные, операции над ними и ожидаемые результаты.
+1
FallenGameR #
Я не претендую на новизну идеи. С Fitnesse не знаком и его возможностей не знаю. Но поглядев на их Wiki могу сказать, что движок Fitnesse очень отличен от моего:

1) Отношение к тестовым данным.

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

У меня тестовые данные находятся либо в самой спецификации, либо в отдельном файле. В обоих случаях файлы хранятся в DLL тестового проекта как Embedded Resource. Это позволяет:
— Включать интеграционные тесты вместе со всеми данными в систему контроля версий.
— Всегда иметь под рукой спецификации и тестовые данные, когда работаешь в студии.

2) Считывание пользовательских типов данных.

Насколько просто в Fitnesse считать из спецификации данные такого вида?

$Vertices =
(0;0) (4;0) (4;3) (0;3)
(0;2) (2;2) (2;3) (0;3)
(0;1) (3;1) (3;3) (0;3)

Это коллекция точек в двумерном пространстве.
Мой движок заточен на расширяемость пользовательскими типами данных.

3) Ориентированность

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

Это отличие продиктовано отношением к тестовым данным. У меня сложилось впечатление, что Fitnesse ориентирован на более низкоуровневое unit-тестирование. У меня же упор на интеграционное. Мне сложно объяснить без демонстрации кода. Скоро опубликую статью про мой движок, там покажу что я имею ввиду.

4) Чистота кода

Мой движок не требует объявлять место хранения для свойств в классе. Такого вида код не встречается (http://schuchert.wikispaces.com/FitNesse.Tutorials.1):

private int channel;
public void setChannel(int channel) {
this.channel = channel;
}

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

5) Простой тестовый вывод

У меня простой движок, он не генерирует HTML отчеты. У него простой консольный вывод в котором указано все что требуется программисту для исправления ошибки. При тестировании из-под студии консольный вывод сохраняется в результатах прогона теста.
0
LunarFrog #
Мой первоночальный комментарий — ни в коей мере не критика, просто хотел добавить что существуют готовые решения подобного рода. Я с удовольствием посмотрю на код вашего движка — Fit расширяем и мы постоянно этим пользуемся, адаптируя его для наших проектов. Возможно мы сможем позоимствовать интересные идеи и у вас :)

Исключительно для информации:

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

Fit-тест можно условно разделить на три части: html-спецификация, инфраструктурный код, использующий Fit-framework и собственно тестируемые классы. Инфраструктурный код интерпретирует получаемые данные, вызывает тестируемый код и возвращает результаты, в коде программы для поддержки Fit ничего менять не надо.
Основной метод отчота — html, но консольные утилиты Fit позволяют интерпретировать эти отчеты, выводить результаты на консоль и использовать Fit-тесты в автоматическом билде.

0
WondeRu #
А как же нагрузочное тестирование?
0
FallenGameR #
Приведена классификация по критерию «степень изолированности кода». У меня это явно указано.
Классифицировать можно по разным критериям.
0
Victor435 #
Институтский курс и жизнь — понятия несовместимые, как правило :-)
0
WondeRu #
В своих проектах мы делаем нагрузочное тестирование, т.к. в требованиях четко прописаны тайминги и количество одновременных пользователей
0
FallenGameR #
Еще раз повторяю — нагрузочное тестирование из другой классификации.
en.wikipedia.org/wiki/Software_testing#Non_Functional_Software_Testing

Один и тот же тест можно отнести в разные группы по разным срезам классификаций. Нагрузочное тестирование, как правило, происходит либо при системном тестировании, либо при интеграционном.
0
ulu #
М-да, сколько разработчиков, столько и определений разных видов тестов…

Лично мне (не будут претендовать на «самое лучшее определение») ближе примерно такая идея: Unit — это не отдельный класс, а *один или несколько* классов, выполняющих определенную задачу. Есть для этого понятие Component. При этом нас не должно интересовать, как общаются между собой классы внутри компоненты (компонента?), и мы это не тестируем. В результате, рефакторинг не обвалит наши тесты.

«Фактически это тестирование методов какого-то класса программы в изоляции от остальной программы.» Есть два подхода. Девелоперский, когда существующая (или планируемая) структура кода первична, а тесты пишутся «под нее». Тут мы будем иметь тестирование того *как* работает наша программа. И «Пользовательский» или «функциональный» подход, когда тесты пишутся, исходя из требований. И мы тестируем *что* делает наша программа.

Интеграционное тестирование (опять-таки, мое субъективное понимание этого термина) — проверка того, как наши компоненты работают вместе. Необязательно устраивать проверку всех возможных сочетаний входных данных, достаточно проверить основные. При этом все остальные компоненты, а также внешние зависимости, отсекаются мок-объектами. Или, например, используется тестовая база данных (типа in-memory SQLite).

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.