Pull to refresh

Moles — Isolation Framework от Microsoft Research или как сделать юнит-тестирование удобнее

Reading time3 min
Views2K
Иногда случается, что стройную и продуманную систему юнит-тестов душит сильная связность компонентов — особенно этим грешит legacy код, изначально не предназначенный для тестирования. Рефакторинг, конечно, спасает — но не всегда можно рефакторить. Одной из проблем, возникающих при создании юнит-тестов может стать использование static методов или non-virtual методов, которые должны быть перегружены для успешного написания тестов. Поможет в этом проект от Microsoft Research — Moles.

Для начала рассмотрим пример (он намеренно упрощен) — есть класс PaymentsCore который содержит метод AcceptPayment — регистрирующий платеж от пользователя. В случае, если платеж невозможен из-за недостатка средств — метод выбрасывает исключение. Задача — написать юнит-тест, который проверит функциональность метода. На вход методу подаются — ID пользователя и сумма платежа. Вот здесь и кроется подвох — данные пользователя, соответствующие переданному ID, получаются с помощью static метода некого класса CacheManager (который обращается к базе данных, помещает объект в кэш приложения, обновляет кэш в зависимости от условий — и т.п.). Да, можно спорить, что архитектура построена не совсем верно — но будем считать, что это legacy код (возможно, что исходный код CacheManager вовсе не доступен). Очевидно, что чтобы избавится от кэша, БД, зависимости от данных поможет mock-объект. Однако, до недавнего времени, единственный mock-фреймворк который позволял перекрывать static методы — это TypeMock Isolator (дорогой коммерческий продукт). Мы же рассмотрим как применить Microsoft Research Moles.
Для начала, создадим новый тестовый проект, добавим ссылку на сборку с приложением и напишем простейший тест для метода AcceptPayment.
[TestMethod]
[ExpectedException(typeof(AmountException))]
public void AcceptPaymentOverdraftTest()
{
  PaymentsApp.PaymentsCore core = new PaymentsCore();
  core.AcceptPayment(1, 200);
}


Тест выполняется если в базе есть клиент с ID равным 1, если сумма средств на его счете не превышает 200 — тест завязан на множество внешних параметров — и, по большому счету, он бесполезен. Настало время mockнуть нужные static методы и создать идеальную среду для выполнения теста.
Скачиваем Moles (будте внимательны, это x86 версия, для x64 — ссылка в Description) и устанавливаем. Возвращаемся в Visual Studio — время писать код.
Итак, первый шаг – добавить в проект конфигурацию для Moles. К тестовому проекту добавляем New item с типом Moles and stubs for testing, в качестве имени указываем PaymentsApp.moles. Содержимое файла будет весьма простым
<?xml version="1.0" encoding="utf-8" ?>
<Moles xmlns="http://schemas.microsoft.com/moles/2010/">
 <Assembly Name="PaymentsApp" />
</Moles>

Здесь указаны сборки, для которых будут сгенерены заглушки. В Solution Explorer так же можно увидеть все сгенерированные файлы, все сборки будут автоматически добавлены в References.

Теперь можно переходить непосредственно ко второму этапу – написанию заглушек. Возвращаемся к коду тестового метода и добавим в начале:
using PaymentsApp.Moles;

Это пространство имен находится в сборке, сгенерированной по конфигурации и содержит для каждого класса оригинальной сборки специальный прокси-класс – начинающийся с префикса M. Т.е. интересующий нас класс CacheManager (который и содержит коварные static методы) будет называться MCacheManager. Для методов будут сгенерированы свойства с именем <оригинальноеИмя><типПараметра1><тип Параметра2>… Таким образом, интересующее нас свойство – GetClientInt32. Свойство имеет тип Func<int, Client> — принимает делегат, который и будет выполнятся вместо оригинального метода CacheManager.GetClient.
Итак, добавим в начало тестового метода строчку:
MCacheManager.GetClientInt32 = id => new Client("Test", 100);

Однако, запускать тест еще рано – нужно указать для данного метода аттрибут HostType чтобы разрешить использование Moles в рамках метода.
[HostType("Moles")]

Итак, итоговый вид тестового метода:
[TestMethod]
[HostType("Moles")]
[ExpectedException(typeof(AmountException))]
public void AcceptPaymentOverdraftTest()
{
  MCacheManager.GetClientInt32 = id => new Client("Test", 100);
  PaymentsApp.PaymentsCore core = new PaymentsCore();
  core.AcceptPayment(1, 200);
}

Теперь вместо static метода класса CacheManager будет выполнятся наш делегат, возвращающий нужный mock-объект. Т.е. тест теперь избавлен от зависимости от БД, кэша и т.п. – и даже без модификаций кода.

Ссылки:
1) Moles — Isolation framework for .NET
2) Видео – быстрый старт с Moles (5 min)
3) TypeMock Isolator
4) Исходный код демонстрационного проекта
Tags:
Hubs:
Total votes 44: ↑30 and ↓14+16
Comments12

Articles