Что такое mock в тестировании

PHPUnit: Mock объекты

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

В качестве используемого примера возьмем следующее описание класса:

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

В простейшем случае для проверки логики мы можем отнаследоваться от указанного класса, подменить заглушками методы, которые обращаются к неподключенным устройствам, и провести модульное тестирование на экземпляре потомка. Примерно так же реализованы Mock объекты в PHPUnit, где при этом предоставляется дополнительное удобство в виде встроенного API.

Получение Mock объекта

Для получения экземпляра Mock объекта используется метод getMock():

Как видите, получить нужный нам Mock объект очень просто. По-умолчанию, все методы в нем будут подменены заглушками, которые ничего не делают и всегда возвращают null.

Параметры вызова getMock

Передача строителю getMock() в качестве второго аргумента значения null приведет к тому, что будет возвращен Mock объект вообще без подмен.

getMockBuilder

Для тех, кому приятнее писать в цепном стиле, PHPUnit предлагает соответствущий конструктор:

Цепочка всегда должна начинаться с метода getMockBuilder() и закачиваться методом getMock() — это единственные звенья цепи, которые являются обязательными.

Дополнительные способы получения Mock объектов

Ожидание вызова метода

PHPUnit позволяет нам контроллировать количество и порядок вызовов подмененных методов. Для этого используется конструкция expects() с последующим указанием нужного метода при помощи method(). В качестве примера обратимся к классу, приведенному в начале статьи, и напишем для него вот такой тест:

Результат выполнения этого теста будет успешным, если при вызове метода process() произойдет однократный вызов трех перечисленных методов: getTemperature(), getWord(), showWord(). Обратите внимание, что в тесте проверка вызова getWord() стоит после проверки вызова showWord(), хотя в тестируемом методе наоборот. Все верно, ошибки здесь нет. Для контроля порядка вызова методов в PHPUnit используется другая конструкция — at(). Поправим немного код нашего теста так чтобы PHPUnit проверил заодно очередность вызова методов:

Помимо упомянутых once() и at() для тестирования ожиданий вызовов в PHPUnit есть также следующие конструкции: any(), never(), atLeastOnce() и exactly($count). Их названия говорят сами за себя.

Переопределение возвращаемого результата

Безусловно самой полезной функцией Mock объектов является возможность эмуляции возвращаемого результата подмененными методами. Снова обратимся к методу process() нашего класса. Мы видим там обращение к датчику температуры — getTemperature(). Но мы также помним, что на самом деле датчика у нас нет. Хотя даже если бы он у нас был, не будем же мы охлаждать его ниже 15 градусов или нагревать выше 25 для того, чтобы протестировать все возможные ситуации. Как вы уже догадались, в этом случае на помощь к нам приходят Mock объекты. Мы можем заставить интересующий нас метод вернуть любой результат какой захотим при помощи кострукции will(). Вот пример:

Проверка указанных аргументов

Еще одной полезной для тестирования возможностью Mock объектов является проверка аргументов, указанных при вызове подмененного метода, при помощи конструкции with():

Теперь, когда мы полностью ознакомились с возможностями Mock объектов в PHPUnit, мы можем провести окончательное тестирование нашего класса:

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

Подмена статических методов

Начиная с PHPUnit версии 3.5 стала возможной подмена статических методов при помощи статической конструкции staticExpects():

Источник

Написание Unit-тестов. Mocking объектов

Кому нужны модульные тесты? Не Вам — Ваш код идеален. Но все же, Вам просто нужно прочитать эту статью, которая должна больше рассказать о написании модульных тестов на Swift’е. Вдруг это Вам в дальнейшем пригодиться.

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

Модульное тестирование работает с изолированными “микрокомпонентами”. Зачастую Вам нужно «мокировать» классы – то есть обеспечить фейк функциональной реализацией, чтобы изолировать специфический микрокомпонент, таким образом, он сможет быть протестирован. В Objective-C существует несколько сторонних фреймворков, которые помогают это реализовать. Но они еще не доступны в Swift’е.

В этом руководстве Вы научитесь, писать свои собственные mock-обьекты, fakes и stub’ы, чтобы покрыть тестами давольно простое приложение, которое поможет вам запомнить дни рождения ваших друзей.

Давайте начнем

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

Скомпилируйте и запустите приложение, и затем проверьте, как оно работает. Нажмите кнопку plus и затем добавьте John Appleseed в общий список контактов:

Что такое mock в тестировании. Смотреть фото Что такое mock в тестировании. Смотреть картинку Что такое mock в тестировании. Картинка про Что такое mock в тестировании. Фото Что такое mock в тестировании

Для хранения контактов, приложения использует Core Data.

Что такое mock в тестировании. Смотреть фото Что такое mock в тестировании. Смотреть картинку Что такое mock в тестировании. Картинка про Что такое mock в тестировании. Фото Что такое mock в тестировании

Не паникуйте! Вам не нужен опыт работы с Core Data для этого урока; для этого Вам не нужно иметь никаких специальных навыков.

Преимущества и недостатки модульного тестирования

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

Хотя и нет идеального решения, есть светлая сторона – написание тестов имеет следующие преимущества:

Базовая структура приложений

Большое количество кода в приложения основывается на шаблоне Master-Detail Application с включенной Core Data. Но есть некоторые существенные улучшения по шаблону кода. Откройте проект в Xcode и посмотрите на навигатор проекта:

Что такое mock в тестировании. Смотреть фото Что такое mock в тестировании. Смотреть картинку Что такое mock в тестировании. Картинка про Что такое mock в тестировании. Фото Что такое mock в тестировании

Примите во внимание следующие детали:

В этом случае протокол определён в PeopleListDataProviderProtocol.swift; откройте его и посмотрите. Класс, соответствующий этому протоколу, должен иметь свойства managedObjectContext и tableView, и должен определить методы addPerson(_:) и fetch(). Кроме того, он должен соответствовать протоколу UITableViewDataSource.

Контроллер представления PeopleListViewController имеет свойство dataProvider, что соответствует протоколу PeopleListDataProviderProtocol. Это свойство установлено в экземпляр PeopleListDataProvider в файле AppDelegate.swift.

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

PeopleListDataProvider ответственен за заполнение табличного представления и обращение к Core Data.

Примечание: Несколько классов и методов в проекте объявлены как публичные; так, что таргет для тестов может получить доступ к классам и методам. Таргет для тестов находится вне модуля приложения. Если вы не добавляете модификатор доступа, классы и методы определяются, как internal. Это означает, что они доступны только в том же модуле. Чтобы получить доступ к ним снаружи модуля (например, от таргета для тестов), Вы должны добавить модификатор доступа public.

Ну что ж, наступило время для написания нескольких тестов!

Mock-объекты позволяют вам проверить, выполнен ли вызов методов или установлено ли свойство. Например, на viewDidLoad() из PeopleListViewController, табличное представление установлено в свойство tableView из dataProvider.

Вы напишите тест, чтобы проверить, что же на самом деле происходит.

Подготовка приложения к тестированию

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

Выберите проект в навигаторе проекта, затем выберите Build Settings в таргете тестирования Birthdays. Найдите Defines Module, и измените настройки установив в Yes, как показано ниже:

Что такое mock в тестировании. Смотреть фото Что такое mock в тестировании. Смотреть картинку Что такое mock в тестировании. Картинка про Что такое mock в тестировании. Фото Что такое mock в тестировании

Затем выберите папку BirthdaysTests и перейдите к File\New\File…. Выберите iOS\Source\Test Case Class, затем нажмите Next, назовите его PeopleListViewControllerTests, убедитесь, что вы создаёте файл Swift, снова нажмите Next, затем нажмите Create.

Если Xcode предлагает Вам создавать объединяющий заголовок, выберите No. Это — ошибка в Xcode, которая происходит, когда нет файлов в таргете, и Вы добавляете новый Swift файл.

Откройте недавно созданный PeopleListViewControllerTests.swift. Импортируйте модуль, который вы только что включили, добавив оператор import Birthdays прямо после других операторов импорта, как показано ниже:

Удалите следующие два шаблонных метода:

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

Добавьте следующую строку в начало PeopleListViewControllerTests

Затем замените метод setUp() следующим кодом:

Он использует storyboard для создание экземпляра PeopleListViewController, и присваивает его в проперти viewController.

Выберите Product\Test; Xcode компилирует проект и запускает любые существующие тесты. Хотя у вас ещё нет тестов, это позволит Вам убедиться, что всё настроено правильно. После нескольких секунд Xcode должен сообщить, что все тесты прошли успешно.

Вы сейчас на пути к созданию вашего первого mock объекта.

Написание первого Mock обьекта

Поскольку вы собираетесь работать с Core Data, добавьте следующий импорт вверх PeopleListViewControllerTests.swift, сразу после строки import Birthdays:

Затем, добавьте следующий код в определение класса PeopleListViewControllerTests:

Это похоже на довольно сложный mock-обьект. Однако, это – просто требуемый абсолютный минимум, поскольку вы собираетесь присвоить экземпляр этого mock-класса к свойству PeopleListViewController dataProvider. Ваш mock-класс также должен соответствовать PeopleListDataProviderProtocol, а также протоколу UITableViewDataSource.

Выберите Product\Test; проект будет скомпилирован повторно, а ваши тесты прошли на Ура. Но теперь у Вас все настроено для первого модульного тестирования с помощью mock обьекта.

Следует разделить модульное тестирование на три части, назвав их, given, when, и then. ‘Given’ настраивает среду; ‘when’ выполняет код, который вам требуется протестировать; а ‘then’ проверяет ожидаемый результат.

Ваш тест проверят, что свойство tableView поставщика данных установлено после того, как метод viewDidLoad() был выполнен.

Добавьте следующий тест в PeopleListViewControllerTests:

Затем снова выберите Product\Test; как только тесты завершатся, откройте навигатор (Cmd+5 – удобная быстрая клавиша). И Вы должны увидеть следующее:

Что такое mock в тестировании. Смотреть фото Что такое mock в тестировании. Смотреть картинку Что такое mock в тестировании. Картинка про Что такое mock в тестировании. Фото Что такое mock в тестировании

Ваше первое тестирование с помощью mock-обьекта прошло успешно!

Тестирование метода addPerson(_:)

Следующее тестирование состоит в том, чтобы убедиться, что выбор контакта из списка вызовет метод addPerson(_:)

Добавьте следующее свойство в класс MockDataProvider:

Затем замените метод addPerson(_:) на следующий:

Теперь, когда вы вызовете addPerson(_:), вы зарегистрируете его в экземпляре, присвоив значение true для MockDataProvider.

Вам придётся импортировать фреймворк AddressBookUI до того, как вы сможете добавить метод для тестирования этого поведение.

Добавьте следующий импорт в PeopleListViewControllerTests.swift:

Теперь добавьте следующий тестовый метод в остальную часть тестовых сценариев:

Итак, что происходит здесь?

Выберите Product\Test, чтобы запустить тесты. Так оказывается, это довольно легкое задание!

Но подождите! Не торопитесь! Откуда вы знаете, что тесты на самом деле проверяют то, что вы думаете, что они тестируют?

Что такое mock в тестировании. Смотреть фото Что такое mock в тестировании. Смотреть картинку Что такое mock в тестировании. Картинка про Что такое mock в тестировании. Фото Что такое mock в тестировании

Проверка ваших тестов

Быстрый способ проверить, что тест фактически проверяет что-то, состоит в том, чтобы удалить объект, который проверяет тест.

Откройте PeopleListViewController.swift и закоментируйте следующую строку peoplePickerNavigationController(_:didSelectPerson:):

Запустите тесты снова; последний тест, который вы только что написали, должен сейчас завершиться неудачно. Шедеврально – вы знаете, что ваши тесты на самом деле проверяют что-то. Следует проверить свои тесты; по крайней мере, Вы должны проверить самые сложные тесты, чтобы убедиться, что они работают.

Что такое mock в тестировании. Смотреть фото Что такое mock в тестировании. Смотреть картинку Что такое mock в тестировании. Картинка про Что такое mock в тестировании. Фото Что такое mock в тестировании

Раскомментируйте строку, чтобы вернуть код в рабочее состояние; запустите тесты снова, чтобы убедиться, что всё работает.

Mocking Apple Framework Classes

Вы могли использовать синглтоны, такие как NSUserDefaults.standardUserDefaults() и NSNotificationCenter.defaultCenter(), но как вы бы протестировали значение которое установлено по умолчанию? Apple не позволяет вам проверить состояние этих классов.

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

Чтобы обойти эти ограничения, вы можете использовать mock-обьекты вместо этих синглтонов.

Примечание: При замене классов Apple на mock-обьект, очень важно проверить взаимодействие с тем классом, а не с поведением того класса, поскольку детали реализации могут измениться в любой момент.

Скомпилируйте и запустите приложение; добавьте John Appleseed и David Taylor в список людей и переключите сортировку между «Last Name» и «First Name». Вы увидите, что порядок контактов в списке зависит от сортировки.

Код, который отвечает за сортировку, находиться в методе changeSort() в PeopleListViewController.swift:

Он добавляет выбранный сегментный индекс для сортировки по ключу в NSUserDefaults и вызывает метод fetch(). Метод fetch() должен прочитать этот новый порядок сортировки с NSUserDefaults и обновить список контактов, продемонстрированный в PeopleListDataProvider:

PeopleListDataProvider использует NSFetchedResultsController, чтобы произвести выборку данных из Core Data. Чтобы заменить сортировку списка, fetch() создаёт массив с помощью дескрипторов сортировки и устанавливает его в запрос выборки выбранного контроллера результатов. Затем он выполняет выборку, чтобы обновить список и вызвать метод reloadData() для таблицы.

Вы сейчас добавите тест, чтобы убедиться, что предпочтительный порядок сортировки пользователя правильно установлен в NSUserDefaults.

Откройте PeopleListViewControllerTests.swift и добавьте следующее определение классов ниже определения класса Mockdataprovider:

MockUserDefaults является подклассом NSUserDefaults; у него есть булевое свойство sortWasChanged со значением по умолчанию false. Он также переопределяет метод setInteger(_:forKey:), который меняет значение sortWasChanged на true.

Добавьте следующий тест ниже последнего теста в классе PeopleListViewControllerTests:

Запустите свой пакет тестов – все они должны успешно завершиться.

Что относительно случая, когда у Вас есть действительно сложный API или фреймворк, но Вы действительно хотите протестировать небольшой компонент, не «закапывайтесь» глубоко в фреймворк!

Именно тогда Вы “подделываете” его, а не создаете! :]

Написания Fakes объектов

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

В случае приложения вам не требуется добавлять записи и выбирать их с Core Data. Итак, вместо этого вы подделаете Core Data. Звучит немного устрашающи, не так ли?

Выберите папку BirthdaysTests и перейдите к File\New\File…. Выберите шаблон iOS\Source\Test Case Class и нажмите Next. Назовите ваш класс PeopleListDataProviderTests, нажмите Next и затем Create.

Снова удалите ненужные тесты в созданном тестовом классе:

Добавьте два следующие импорта в новый класс:

Затем добавьте следующие свойства:

Свойства содержат основные компоненты, которые используются в стеке Core Data. Чтобы приступить к работе с Core Data, изучите наше руководство Core Data Tutorial: Начало

Добавьте следующий код в метод setUp():

Вот что происходит в вышеупомянутом коде:

Добавьте следующие два свойства в PeopleListDataProviderTests:

Сейчас добавьте следующий код в конец метода setUp():

Это настраивает табличное представление, инстанцируя контроллер представления с storyboard и создает экземпляр PersonInfo, который будет использоваться в тестах.

Когда тест будет выполнен, Вы должны будете «cбросить» контекст управляемого объекта.

Замените метод tearDown() следующим кодом:

Этот код устанавливает managedObjectContext в значение nil, чтобы освободить память, и удалить persistent store из store coordinator. Вы можете запустить каждый тест с новым тестовым хранилищем.

Теперь — Вы можете написать тест! Добавьте следующий тест в свой тестовый класс:

Этот тест проверяет, что хранилище не равно nil. Запустите новый тест – всё должно пройти успешно.

Следующий тест проверит, обеспечивает ли источник данных ожидаемое количество строк.

Добавьте следующий тест в тестовый класс:

Сначала добавьте контакт в тестовое хранилище, затем подтвердите, что количество строк равно 1.
Запустите тесты – они должны все успешно выполниться.

Создавая фейковое “постоянное” хранилище, вы обеспечиваете быстрое тестирование и позволяете диску оставаться чистым, таким образом, Вы можете быть уверенны, что, приложение при запуске будет работать, ожидаемо.

Написал тест Вы также можете проверить количество разделов и строк после того, как вы добавили два или более тестовых контактов; это всё зависит от уровня уверенности, которого вы пытаетесь достичь в проекте.

Если вы когда-либо работали сразу с несколькими командами над проектом, вы знаете, что не все части проекта готовы в одно и то же время, но вам уже нужно протестировать ваш код. Но как Вы можете протестировать часть своего кода на что-то, что не существует, например веб-сервиса?

Stub’ы придут вам на помощь!

Stub’ы подделывают ответ на вызовы метода объекта. Вы будете использовать стабы, чтобы протестировать Ваш код вызова веб-сервиса, который может быть еще в разработке.

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

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

Откройте APICommunicatorProtocol.swift; этот протокол объявляет два метода для получения контактов с веб-сервиса и добавление контактов.

Вы могли бы переместить экземпляры Person, но для этого будет нужен другой контекст управляемого объекта. Использование структур стало намного проще в этом случае.

Вы сейчас будете создавать stub’ы для поддержки взаимодействия контроллера представления с экземпляром APICommunicator.

Откройте PeopleListViewControllerTests.swift и добавьте следующее определение класса в пределах класса PeopleListViewControllerTests:

Теперь пора протестировать Ваш Stub API, чтобы удостоверится, что все контакты, которые возвращаются из API, добавлены в хранилище, когда Вы вызываете метод addPerson()

Добавьте следующий тестовый метод в PeopleListViewControllerTests:

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

Вы научились, как писать mock-обьекты, fakes и stub’ы для тестирования микрокомпонентов в своём приложении и разобрались, как работает XCTest в Swift’е.

В этой статье представлено только начальное понимание тестов; я уверен, что у вас уже появились идеи для написания тестов для своих приложений.

Для большей информации о модульном тестировании изучите Test Driven Development (TDD) и Behavior Driven Development (BDD). Это методологии разработки приложений (и, откровенно говоря, представляют совершенно новое мышление), где Вы пишете тесты, прежде чем Вы напишете код.

Модульное тестирование является только одной частью полного тестового пакета; комплексные тестирования – следующий логический шаг. Простым способом, чтобы начать работать с комплексным тестированием является использование UIAutomation. Если вы серьёзны настроены тестировать свои приложения, тогда Вам нужно использовать UIAutomation!

Источник

vertigra / mock-object-conspect.md

Введение в mock-объекты. Классификация.

Часто тестируемый метод может вызывать методы других классов, которые в данном случае тестировать не нужно. Unit-тест потому и называется модульным, что тестирует отдельные модули, а не их взаимодействие. Причем, чем меньше тестируемый модуль – тем лучше с точки зрения будущей поддержки тестов. Для тестирования взаимодействия используются интеграционные тесты, где вы уже тестируете скорее полные use cases, а не отдельную функциональность.

Однако наши классы очень часто используют другие классы в своей работе. Например, слой бизнес логики (Business Logic layer) часто работает с другими объектами бизнес логики или обращается к слою доступа к данным (Data Access layer). В трехслойной архитектуре веб-приложений это вообще постоянный процесс: Presentation layer обращается к Business Logic layer, тот, в свою очередь, к Data Access layer, а Data Access layer – к базе данных. Как же тестировать подобный код, если вызов одного метода влечет за собой цепочку вплоть до базы данных?

В таких случаях на помощь приходят так называемые mock-объекты, предназначенные для симуляции поведения реальных объектов во время тестирования. Вообще, понятие mock-объект достаточно широко: оно может, с одной стороны, обозначать любые тест-дублеры (Test Doubles) или конкретный вид этих дублеров – mock-объекты.

Понятие тест-дублеров введено неким Gerard Meszaros в своей книге «XUnit Test Patterns». Джерард и Мартин делят все тест-дублеры на 4 группы:

Предположим, что вам нужно протестировать метод Foo() класса TestFoo, который делает вызов другого метода Bar() класса TestBar. Предположим, что метод Bar() принимает какой-нибудь объект класса Bla в качестве параметра и потом ничего особого с ним не делает. В таком случае имеет смысл создать пустой объект Bla, передать его в класс TestFoo (сделать это можно при помощи широко применяемого паттерна Dependency Injection или каким-либо другим приемлемым способом), а затем уже Foo() при тестировании сам вызовет метод TestBar.Bar() с переданным пустым объектом. Это и есть иллюстрация использования dummy-объекта в unit-тестировании.

Метод Bar() выполняет какие-то действия с ним (допустим, Bar() сохраняет данные в базу или вызывает веб-сервис, а мы этого не хотим). В таких случаях наш объект класса TestBar должен быть уже не таким глупым. Мы должны научить его в ответ на запрос сохранения данных просто выполнить какой-то простой код (допустим, сохранение во внутреннюю коллекцию). В таких случаях можно выделить интерфейс ITestBar, который будет реализовывать класс TestBar и наш дополнительный класс FakeBar. При unit-тестировании мы просто будем создавать объект класса FakeBar и передавать его в класс с методом Foo() через интерфейс. Естественно, при этом класс Bar будет по-прежнему создаваться в реальном приложении, а FakeBar будет использован лишь в тестировании. Это иллюстрация fake-объекта

Stub-объекты (стабы) – это типичные заглушки. Они ничего полезного не делают и умеют лишь возвращать определенные данные в ответ на вызовы своих методов. В нашем примере стаб бы подменял класс TestBar и в ответ на вызов Bar() просто бы возвращал какие-то левые данные. При этом внутренняя реализация реального метода Bar() бы просто не вызывалась. Реализуется этот подход через интерфейс и создание дополнительного класса StubBar, либо просто через создание StubBar, который является унаследованным от TestBar. В принципе, реализация очень похожа на fake-объект с тем лишь исключением, что стаб ничего полезного, кроме постоянного возвращения каких-то константных данных не требует. Типичная заглушка. Стабам позволяется лишь сохранять у себя внутри какие-нибудь данные, удостоверяющие, что вызовы были произведены или содержащие копии переданных параметров, которые затем может проверить тест.

Mock-объект (мок), в свою очередь, является, грубо говоря, более умной реализацией заглушки, которая уже не просто возвращает предустановленные данные, но еще и записывает все вызовы, которые проходят через нее, чтобы вы могли дальше в unit-тесте проверить, что именно эти методы вот этих вот классов были вызваны тестируемым методом и именно в такой последовательности (хотя учет последовательности и строгость проверки, в принципе, настраиваемая вещь). То есть мы можем сделать мок MockFoo, который будет каким-то образом вызывать реальный метод Foo() класса TestFoo и затем смотреть, какие вызовы тот сделал. Или сделать мок MockBar и затем проверить, что при вызове метода Foo() реально произошел вызов метода Bar() с нужными нам параметрами.

Unit-тестирование условно делится на два подхода:

То есть в state-based testing нас интересует в основном, в какое состояние перешел объект после вызова тестируемого метода, или, что более часто встречается, что в реальности вернул наш метод и правилен ли этот результат. Подобные проверки проводятся при помощи вызова методов класса Assert различных unit-тест фреймворков: Assert.AreEqual(), Assert.That(), Assert.IsNull() и т.д.

В interaction testing нас интересует прежде всего не статическое состояние объекта, а те динамические вызовы методов, которые происходят у него внутри. То есть для нашего примера с классами TestFoo и TestBar мы будем проверять, что тестируемый метод Foo() действительно вызвал метод Bar() класса TestBar, а не то, что он при этом вернул и в какое состояние перешел. Как правило, в случае подобного тестирования программисты используют специальные mock-фреймворки (TypeMock.Net, EasyMock.Net, MoQ, Rhino Mocks, NMock2), которые содержат определенные конструкции для записи ожиданий и их последующей проверки через методы Verify(), VerifyAll(), VerifyAllExpectations() или других (в зависимости от конкретного фреймворка).

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

Пример тестирования с использованием стаба для state-based тестирования:

Пара пояснений по коду. Сначала мы создаем объект типа Order, затем – стаб для класса Warehouse. После этого мы при помощи mock-фреймворка говорим, что при вызове метода HasInventory с определенными параметрами этот метод должен нам вернуть true. Аналогичным образом переопределяем поведение метода Remove (а то еще вызовет реальный и будет бяка). Далее идет вызов метода Fill() с переданным стабом, после чего проверяется, что свойство IsFilled установлено в true. Как видите, ничего сложного. Однако данный тест обладает некоторыми недостатками. Во-первых, непонятно, что делать, если в тестируемом объекте нет свойства, аналогичного IsFilled. Как проверять правильность выполнения кода? Во-вторых, непонятно, что случится, если программист удалит или закомментирует вызов следующей строчки в коде метода Fill():

IsFilled устанавливается в true, тест проходит, но код-то уже не работает!

Обе эти проблемы легко разрешаются, если мы воспользуемся interaction тестированием с использованием мока. Для этого напишем другой тест:

Начало теста аналогичное, затем идет создание мока Warehouse, после чего идет несколько вызовов метода Expect с теми же параметрами, что и в предыдущем тесте. При помощи этого метода мы говорим моку, что мы ожидаем вызова этих методов с такими параметрами и нам в ответ на их вызовы нужно вернуть такие-то значения. Затем идет вызов метода Replay(), который переводит мок из режима записи ожиданий в режим их проверки, то есть запуска тестового метода. Все моки имеют несколько режимов работы (Record, Replay, Verify), это распространенный подход. Далее непосредственно запуск, проверка IsFilled и вызов нового для нас метода VerifyAllExpectations(). Последний как раз и делает всю работу по проверке вызовов методов, параметров и т.д. Теперь, если метод Remove оказался закомментированным, тест не пройдет. Кроме того, нам уже не так важна проверка состояния объекта Order. Если бы свойства IsFilled не было, ничего бы не изменилось, а так мы лишь проверяем, что оно было установлено в соответствии с алгоритмом. Теперь немного поэкспериментируем с кодом. Что, если мы уберем второй Expect или поменяем их местами? Есть несколько режимов строгости проверки, которые задаются через конструктор класса Mock, который также можно использовать для создания мока. В Rhino Mocks есть три уровня строгости: Loose, Strict и Default (Loose). В Loose-режиме мок проверяет лишь то, что все ожидаемые методы были вызваны из тестируемого метода, в то время как в Strict-режиме проверяется также, что не было любых других вызовов и что порядок вызову соответствует порядку ожиданий. В других фреймворках иногда есть и другие режимы. Таким образом, в нашем случае при изменении порядка тест бы прошел, но в Strict-режиме – уже нет. Еще один момент, который показывает отличие методов Expect от методов Stub (в моке они также доступны): методы, зарегистрированные в моке при помощи метода Stub невидимы для метода VerifyAllExpectations. То есть, если нужна проверка вызовов – используйте Expect. Также стоит отметить, что при помощи дополнительных методов типа Return вы можете не только указывать возвращаемые значения, но еще генерировать exception’ы (Throw), вызывать настоящий метод (CallOriginalMethod), задавать ограничения на параметры (Constraints), вызывать дополнительные методы (Callback, Do), работать со свойствами и событиями. В общем, список потрясающий.

Источник

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

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