Mocking framework что это

Обновлено: 04.07.2024

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

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

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

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

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

  • state - based testing , в котором мы тестируем состояние объекта после прохождения unit-теста
  • interaction (behavioral) testing , в котором мы тестируем взаимодействие между объектами, поведение тестируемого метода, последовательность вызовов методов и их параметры и т.д.

То есть в 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 , NMock 2 ), которые содержат определенные конструкции для записи ожиданий и их последующей проверки через методы Verify(), VerifyAll(), VerifyAllExpectations() или других (в зависимости от конкретного фреймворка).

То есть во многом это отличие можно назвать аналогичным отличию state machine diagram и activity diagram в UML : описывают они, в принципе, одно и то же, но разными способами. Иногда удобнее один, иногда второй.

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

Допустим, у нас есть несколько простых классов:

Вот такой набор классов. Класс Order обращается к классу Warehouse, а тот обращается к базе данных. Предположим, что мы тестируем метод Fill() класса Order. Вот пример тестирования с использованием стаба для state - based тестирования:

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

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

Moq – это простой и легковесный изоляционный фреймврк (Isolation Framework), который построен на основе анонимных методов и деревьев выражений. Для создания моков он использует кодогенерацию, поэтому позволяет «мокать» интерфейсы, виртуальные методы (и даже защищенные методы) и не позволяет «мокать» невиртуальные и статические методы.

ПРИМЕЧАНИЕ
На рынке существует лишь два фрейморка, позволяющих «мокать» все, что угодно. Это TypeMockIsolator и Microsoft Fakes, доступные в Visual Studio 2012 (ранее известные под названием Microsoft Moles). Эти фреймворки, в отличие от Moq, используют не кодогенерацию, а CLR Profiling API, что позволяет вклиниться практически в любой метод и создать моки/стабы даже для статических, невиртуальных или закрытых методов.

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

Проверка состояния (state verification)

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

1. Стаб метода GetCurrentDirectory:

2. Стаб метода GetDirectoryByLoggerName, всегда возвращающий один и тот же результат:

3. Стаб метода GetDirrectoryByLoggerName, возвращающий результат в зависимости от аргумента:

4. Стаб свойства DefaultLogger:

5. Задание поведения нескольких методов одним выражением с помощью “moq functional specification” (появился в Moq v4):

6. Задание поведение нескольких методов с помощью вызова методов Setup («старый» v3 синтаксис):

ПРИМЕЧАНИЕ
Как я уже упоминал, в Moq нет разделения между моками и стабами, однако нам с вами будет значительно проще различать два синтаксиса инициализации заглушек. Так, «moq functional specification» синтаксис может использоваться только для тестирования состояния (т.е. для стабов) и не может применяться для задания поведения. Инициализация же заглушек методом Setup может быть, во-первых, более многословной, а во-вторых, при ее использовании не совсем понятно, собираемся ли мы проверять поведение или состояние.

Проверка поведения (behavior verification)

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

1. Проверка вызова метода ILogWriter.Write объектом класса Logger (с любым аргументом):

2. Проверка вызова метода ILogWriter.Write с заданным аргументами:

3. Проверка того, что метод ILogWriter.Write вызвался в точности один раз (ни больше, ни меньше):

ПРИМЕЧАНИЕ
Существует множество вариантов проверки того, сколько раз вызвана зависимость. Для этого существуют различные методы класса Times: AtLeast(int), AtMost(int), Exactly, Between и другие.

4. Проверка поведения с помощью метода Verify (может быть удобной, когда нужно проверить несколько допущений):

5. Проверка нескольких вызовов с помощью метода Verify().

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

Отступление от темы. Strict vs Loose модели

Moq поддерживает две модели проверки поведения: строгую (strict) и свободную (loose). По умолчанию используется свободная модель проверок, которая заключается в том, что тестируемый класс (Class Under Test, CUT), во время выполнения действия (в секции Act) может вызывать какие угодно методы наших зависимостей и мы не обязаны указывать их все.

Так, в предыдущем примере метод logger.WriteLine вызывает два метода интерфейса ILogWriter: метод Write и SetLogger. При использовании MockBehavior.Strict метод Verify завершится неудачно, если мы не укажем явно, какие точно методы зависимости будут вызваны:

Использование MockRepository

Класс MockRepository предоставляет еще один синтаксис для создания стабов и, что самое главное, позволяет хранить несколько мок-объектов и проверять более комплексное поведение путем вызова одного метода.

2. Использование MockRepository для задания поведения нескольких мок-объектов.
Предположим, у нас есть более сложный класс SmartLogger, которому требуется две зависимости: ILogWriter и ILogMailer. Наш тестируемый класс при вызове его метода Write должен вызвать методы двух зависимостей:

Другие техники

В некоторых случаях бывает полезным получить сам мок-объект по интерфейсу (получить Mock<ISomething> по интерфейсу ISomething). Например, функциональный синтаксис инициализации заглушек возвращает не мок-объект, а сразу требуемый интерфейс. Это бывает удобным для тестирования пары простых методов, но неудобным, если понадобится еще и проверить поведение, или задать метод, возвращающий разные результаты для разных параметров. Так что иногда бывает удобно использовать LINQ-based синтаксис для одной части методов и использовать методы Setup – для другой:

Помимо этого Moq позволяет проверять поведение защищенных методов, тестировать события и содержит некоторые другие возможности.

В этом курсе вы погрузитесь в магию мокито. Вы узнаете о издевательствах, шпионах и частичных издевательствах, а также их соответствующем поведении. Вы также увидите процесс проверки с помощью тестовых двойников и сопоставителей объектов. Наконец, обсуждается разработка через тестирование (TDD) с Mockito, чтобы увидеть, как эта библиотека вписывается в концепцию TDD. Проверьте это здесь !

Содержание

В этом руководстве мы рассмотрим Mockito Mocking Framework и подготовим проект Eclipse для его использования, добавив его в путь к классам.

1. Почему издеваться?

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

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

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

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

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

Для Java доступно несколько макетов, каждая из которых имеет свой синтаксис, свои сильные стороны, свои слабые стороны. В этом уроке мы будем использовать фреймворк Mockito, который является одним из наиболее популярных фреймворков.

2. Введение в Mockito Framework

Mockito — это Mocking Framework, который позволяет очень легко создавать макеты для классов и интерфейсов, с которыми взаимодействует ваш тестируемый класс. Mockito предоставляет очень простой API для создания макетов и определения их поведения. Это позволяет вам очень быстро указать ожидаемое поведение и проверить взаимодействие с вашими макетами.

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

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

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

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

Однако у Mockito есть некоторые ограничения, в том числе

  • Вы не можете издеваться над финальными классами
  • Вы не можете издеваться над статическими методами
  • Вы не можете издеваться над финальными методами
  • Вы не можете смоделировать equals () или hashCode ()

2.1. Быстрый пример заглушки

Представьте, что вы пишете класс, который вызывает API физического датчика температуры. Вы хотите вызвать double getDegreesC() и вернуть одну из следующих строк — «Горячая», «Мягкая», «Холодная» — на основе значения, возвращаемого датчиком. Было бы очень трудно, если не сказать больше, чтобы ваши юнит-тесты контролировали температуру в помещении, чтобы проверить ваши функциональные возможности. Но что, если мы используем Mockito для создания макета, который мы заменяем датчиком?

Часто тестируемый метод может вызывать методы других классов, которые в данном случае тестировать не нужно. 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 группы:

  • Dummy – пустые объекты, которые передаются в вызываемые внутренние методы, но не используются. Предназначены лишь для заполнения параметров методов.
  • Fake – объекты, имеющие работающие реализации, но в таком виде, который делает их неподходящими для production-кода (например, In Memory Database).
  • Stub – объекты, которые предоставляют заранее заготовленные ответы на вызовы во время выполнения теста и обычно не отвечающие ни на какие другие вызовы, которые не требуются в тесте. Также могут запоминать какую-то дополнительную информацию о количестве вызовов, параметрах и возвращать их потом тесту для проверки.
  • Mock – объекты, которые заменяют реальный объект в условиях теста и позволяют проверять вызовы своих членов как часть системы или unit-теста. Содержат заранее запрограммированные ожидания вызовов, которые они ожидают получить. Применяются в основном для т.н. interaction (behavioral) testing.

Предположим, что вам нужно протестировать метод 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, в котором мы тестируем состояние объекта после прохождения unit-теста
  • interaction (behavioral) testing, в котором мы тестируем взаимодействие между объектами, поведение тестируемого метода, последовательность вызовов методов и их параметры и т.д.

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

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

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

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

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

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

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

Один из удобных подходов предусматривает использование имитированных (или пробных) объектов, которые эмулируют функциональность реальных объектов из проекта, но специфическим и управляемым образом. Имитированные объекты позволяют сузить фокус тестов, проверяя только ту функциональность, которая интересует.

Платные редакции Visual Studio включают поддержку создания имитированных объектов с помощью имитаций, но мы предпочитаем применять простую библиотеку под названием Moq, которая работает со всеми редакциями Visual Studio, включая бесплатные.

Сущность проблемы

Чтобы протестировать этот класс, в тестовый проект необходимо добавить новый класс модульного тестирования. Для этого щелкните правой кнопкой мыши на тестовом проекте в окне Solution Explorer и выберите в контекстном меню пункт Add --> Unit Test (Добавить --> Модульный тест). Если в меню Add не содержится пункта Unit Test, выберите вместо него пункт New Item (Новый элемент) и укажите шаблон Basic Unit Test (Базовый модульный тест). Код, помещенный в новый файл, которому среда Visual Studio назначила стандартное имя UnitTest2.cs, показан в примере ниже:

И здесь мы сталкиваемся с трудностью - работа класса LinqValueCalculator зависит от реализации интерфейса IDiscountHelper. В этом примере используется класс MinimumDiscountHelper, который порождает две разных проблемы:

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

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

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

Добавление Moq в проект Visual Studio

Как и в случае Ninject, простейший способ добавления Moq в проект модульного тестирования предусматривает применение интегрированной поддержки Visual Studio для NuGet. Откройте консоль NuGet (Tools --> Library Package Manager --> Package Manager Console) и введите следующую команду:

Аргумент projectname позволяет сообщить NuGet о том, что пакет Moq необходимо установить внутри проекта модульного тестирования, а не в главном приложении.

Добавление имитированных объектов в модульный тест

Синтаксис взаимодействия с Moq поначалу выглядит несколько странным, поэтому мы подробно рассмотрим все этапы данного процесса.

Создание имитированного объекта

Мы создаем строго типизированный объект Mock<IDiscountHelper>, указывающий библиотеке Moq тип, который она будет обрабатывать - разумеется, для нашего модульного теста это интерфейс IDiscountHelper, но можно применять любой тип, который нужно изолировать для улучшения направленности модульных тестов.

Выбор метода

В дополнение к созданию строго типизированного объекта Mock также необходимо указать вид его поведения. Это основа процесса имитации, позволяющая обеспечить установку базового поведения имитированного объекта, которое можно использовать для проверки функциональности целевого объекта в модульном тесте. Желаемое поведение настраивает следующий оператор в модульном тесте:

Метод Setup() позволяет добавить метод в имитированный объект. Библиотека Moq работает с применением LINQ и лямбда-выражений. Когда вызывается метод Setup() библиотека Moq передает интерфейс, реализация которого была запрошена. Это изящно скрыто с помощью "магии" LINQ, в которую мы не будем углубляться. В результате появляется возможность выбора метода для конфигурирования с помощью лямбда-выражения.

В нашем модульном тесте мы хотим определить поведение метода ApplyDiscount() который является единственным методом в интерфейсе IDiscountHelper, а также методом, подлежащим тестированию в классе LinqValueCalculator.

Мы также должны сообщить Moq интересующие значения параметров, что делается с использованием класса It. В классе It определен набор методов, которые применяются с обобщенными параметрами типов. В данном случае мы вызвали метод IsAny(), указав decimal в качестве обобщенного типа. Это указывает Moq на то, что определяемое поведение должно использоваться всякий раз, когда метод ApplyDiscount() вызывается с любым десятичным значением.

В таблице ниже перечислены методы, предоставляемые классом It: все они являются статическими.

Указывает значения типа T, для которых предикат (predicate) возвратит значение true

Указывает любое значение типа T

Срабатывает, если параметр находится между определенными значениями типа T. Последний параметр - это значение перечисления Range, которым может быть Inclusive или Exclusive

Срабатывает, если строковый параметр дает соответствие с указанным регулярным выражением

Позже будет приведен более сложный пример, в котором используются другие методы класса It, а пока что мы будем применять метод IsAny<decimal>(), позволяющий реагировать на любое десятичное значение.

Определение результата

Метод Returns() позволяет указать результат, который Moq будет возвращать при вызове имитированного метода. Тип результата задается с помощью параметра типа, а сам результат - посредством лямбда-выражения. Вот как это было сделано в примере:

За счет вызова метода Returns() с параметром типа decimal мы указываем Moq, что собираемся возвратить значение decimal. Для лямбда-выражения Moq передает значение типа, полученного в методе ApplyDiscount(). В примере мы создаем сквозной метод, в котором возвращается значение, переданное имитированному методу ApplyDiscount(), без выполнения над ним каких-либо операций. Это простейшая разновидность имитированного метода, но вскоре будут приведены более сложные примеры.

Использование имитированного объекта

Последний шаг связан с использованием имитированного объекта в модульном тесте, для чего производится чтение значения свойства Object объекта Mock<IDiscountHelper>:

Подводя итоги примера, свойство Object возвращает реализацию интерфейса IDiscountHelper, в которой метод ApplyDiscount() возвращает значение переданного параметра decimal.

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

Преимущество применения библиотеки Moq подобным образом заключается в том, что наш модульный тест только проверяет поведение объекта LinqValueCalculator и не зависит от каких-либо реальных реализаций интерфейса IDiscountHelper в папке Models. Это означает, что когда тест не прошел, проблема кроется либо в реализации LinqValueCalculator, либо в способе установки имитированного объекта; решить проблему, возникшую в любом из указанных источников, намного проще, чем иметь дело с цепочкой реальных объектов и взаимодействиями между ними.

Создание более сложного имитированного объекта

В предыдущем разделе был продемонстрирован очень простой имитированный объект, однако удобство библиотеки Moq связано с возможностью быстрого построения сложных видов поведения для тестирования разнообразных ситуаций. В примере ниже в файл UnitTest2.cs добавлен новый модульный тест, который имитирует более сложную реализацию интерфейса IDiscountHelper. В действительности библиотека Moq используется для моделирования поведения класса MinimumDiscountHelper:

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

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

Точно такое же поведение использовалось в предыдущем примере, и оно включено здесь из-за того, что порядок, в котором вызывается метод Setup(), влияет на поведение имитированного объекта. Библиотека Moq оценивает виды поведения в обратном порядке, поэтому самые последние вызовы метода Setup() учитываются первыми. Это означает, что вы должны позаботиться о создании имитированных видов поведения в порядке от наиболее общего до самого специфичного.

Условие It.IsAny<decimal> является наиболее общим из числа определенных в этом примере, так что оно применяется первым. Если изменить порядок вызовов Setup(), это поведение захватит все обращения к методу ApplyDiscount() и сгенерирует некорректные имитированные результаты.

Имитация для специфичных значений (и генерация исключения)

Во втором вызове метода Setup используется метод It.Is():

Предикат, переданный методу Is(), возвращает true, если значение, которое передавалось методу ApplyDiscount(), равно 0. Вместо возвращения результата мы используем метод Throws(), который заставляет библиотеку Moq сгенерировать новый экземпляр исключения, указанного с помощью параметра типа.

Мы также применяем метод Is() для захвата значений, превышающих 100:

Метод It.Is() представляет собой наиболее гибкий способ настройки специфических видов поведения для различных значений параметров, поскольку можно использовать любой предикат, который возвращает true или false. Чаще всего этот метод применяется при создании сложных имитированных объектов.

Имитация для диапазона значений

Финальное использование объекта It связано с его методом IsInRange(), который позволяет захватывать диапазон значений параметра:

Данный способ включен ради полноты, но в реальных проектах обычно применяется метод Is() и предикат, которые делают одно и то же.

Библиотека Moq обладает рядом исключительно полезных функциональных средств, использование которых демонстрируются в кратком руководстве Moq Quickstart.

Читайте также: