Mikhail Filippov's Blog

My personal blog.

Тестирование с помощью Microsoft Fakes

В Visual Studio 2012 получил развитие один из продуктов Microsoft Researсh известный под именем Moles. Это mock-фреймворк для облегчения написания юнит-тестов. Моки это специальные объекты заглушки, с помощью которых в реализуют части функциональности приложения, которые необходимы тестируемому методу. Это дает возможность тестировать только логику метода определив статическую логику для его зависимостей.

В .NET существует популярный mock-фреймвок: Moq, но он не позволяет мокировать статические методы. Это является достаточно серьезной проблемой, когда вы тестируете код который зависит к примеру от текущей даты или файловой системы. Проблема заключается в следующем. Предположим у Вас есть код который регистрирует время входа пользователя в систему:

AccountManager.csСсылка на статью
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AccountManager
{
    private List<string> _logs = new List<string>();

    public void SignIn(string user)
    {
        _logs.Add(string.Format(CultureInfo.InvariantCulture, "{0} {1} Sign In", DateTime.Now, user));
    }

    public IList<string> GetLogs()
    {
        return _logs;
    }
}

Напишем тест на метод SignIn:

AccountManagerTests.csСсылка на статью
1
2
3
4
5
6
7
8
9
10
11
[TestClass]
public class AccountManagerTests
{
    [TestMethod]
    public void ShouldBeCorrectLogRecordAfterSignIn()
    {
        var mgr = new AccountManager();
        mgr.SignIn("user1");
        Assert.AreEqual("02/03/2001 01:02:03 user1 SignIn", mgr.GetLogs().Last());
    }
}

Здесь мы сталкиваемся с проблемой тест не проходит потому, что в коде функции используется статическое свойство DateTime.Now, которое всегда выдает текущую дату и время и нет способа указать ей что надо возвращать какое-то предопределенное значение.

До появления Fakes можно было поступить так. Сделать хелпер класс который бы отвечал за обертывание вызова DateTime.Now и передать его в конструктор объекта.

Рассмотрим какие возможности предоставляет нам Microsoft Fakes. В Fakes существует два способа мокирования объектов: Stub и Shim. Они имееют разную реализацию и используются для решения различных типов задач.

  1. Stubs предназначены для мокирования интерфейсов и виртуальных методов не sealed классов. Эту функциональность предоставляют и другие тестовые фреймворки и работает достаточно быстро, потому что основанна на наследовании.

  2. Shims предназначены для мокирования статических, не переопределяемых методов, а также встроенных системных типов. Эта функциональность уникальна для Fakes и позволяет избежать создания множества оберток для сокрытия системных типов. Shims основаны на перезаписи кода в рантайме, и работают достаточно медленно. Давайте посмотрим как использовать обе эти технологии.

Для начала рассмотрим Stubs. Продолжим предыдущий пример. Для выноса из класса AccountManager зависимости DateTime, создадим новый интерфейс IDateTime, который будет определять 1 метод Now(), возвращающий текущую дату и время:

IDateTime.csСсылка на статью
1
2
3
4
public interface IDateTime
{
    DateTime Now();
}

Добавим в основной проект его реализацию:

DateTimeHelper.csСсылка на статью
1
2
3
4
5
6
7
public class DateTimeHelper : IDateTime
{
    public DateTime Now()
    {
        return DateTime.Now;
    }
}

Изменим AccountManager так, чтобы он принимал реализацию интерфейса в конструкторе и использовал её для получения текущего времени:

AccountManager.csСсылка на статью
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AccountManager
{
    private readonly IDateTime _dateTime;
    private List<string> _logs = new List<string>();

    public AccountManager(IDateTime dateTime)
    {
        _dateTime = dateTime;
    }

    public void SignIn(string user)
    {
        _logs.Add(string.Format(CultureInfo.InvariantCulture, "{0} {1} Sign In",
                                _dateTime.Now(), user));
    }

    public IList<string> GetLogs()
    {
        return _logs;
    }
}

Теперь перепишем тест с использованием Stubs, для этого для начала нужно сгенерировать Stubs. Нажмем правой кнопкой на сборку к которой мы хотим получить Stub. И выберем: Add Fakes Assembly.

How to generate Stubs.

После этого у нас появятся сгенерированные Stubs для всех классов сборки. И файл с расширением .fakes с настройками кодогенерации. Теперь в тесте мы можем использовать сгенерированный Stub.

AccountManagerTests.csСсылка на статью
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[TestClass]
public class AccountManagerTests
{
    [TestMethod]
    public void ShouldBeCorrectLogRecordAfterSignIn()
    {
        var stub = new StubIDateTime { Now = () =>

                    new DateTime(2001, 02, 03, 01, 02, 03, DateTimeKind.Local) };
        var mgr = new AccountManager(stub);
        mgr.SignIn("user1");
        Assert.AreEqual("02/03/2001 01:02:03 user1 Sign In", mgr.GetLogs().Last());
    }
}

Запустим тест и видим что он нормально проходит. Stub так же можно использовать для мокирования свойств:

test.csСсылка на статью
1
2
3
4
5
6
7
8
interface IMyInterface {
    int Value { get; set; }
}

var stub = new StubIMyInterface();
int i = 5;
stub.ValueGet = () => i;
stub.ValueSet = (value) => i = value;

Дополнительную информацию о Stubs можно получить в MSDN.

Теперь рассмотрим Shims. В тестовом проекте интерфейс IDateTime служит исключительно для того чтобы была возможность подменить вызов DateTime.Now, давайте посмотрим как с этим справится Shim. Для начала удалим интерфейс и его реализацию, затем вернем на место вызов DateTime.Now.

AccountManager.csСсылка на статью
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AccountManager
{
    private List<string> _logs = new List<string>();

    public void SignIn(string user)
    {
        _logs.Add(string.Format(CultureInfo.InvariantCulture,

                                 "{0} {1} Sign In", DateTime.Now, user));
    }

    public IList<string> GetLogs()
    {
        return _logs;
    }
}

Видим что тест перестал проходить. Теперь добавим в тест изменение поведения DateTime.Now с помощью Shims. Для начала сделаем Add Fakes Assembly для сборки System, это обеспечит генерацию Shim для класса DateTime, а за тем перепишем код теста:

AccountManagerTests.csСсылка на статью
1
2
3
4
5
6
7
8
9
10
11
12
[TestMethod]
public void ShouldBeCorrectLogRecordAfterSignIn()
{
    using (ShimsContext.Create())
    {
        ShimDateTime.NowGet = () => new DateTime(2001, 02, 03, 01, 02, 03,
DateTimeKind.Local);
        var mgr = new AccountManager();
        mgr.SignIn("user1");
        Assert.AreEqual("02/03/2001 01:02:03 user1 Sign In", mgr.GetLogs().Last());
    }
}

Запустим тест и видим, что он проходит. Shim позволяет подменять функциональность любых закрытых методов и избавляет нас от необходимости делать кучу ненужных оберток. Опишу подробнее, что происходит. Все вызовы к DateTime.Now внутри блока using (ShimsContext.Create()) будут перехвачены и подменены mock-объектом. Mock-контекст существует только в рамках этого блока. Дополнительно об использование Shims вы можете почитать в MSDN.

Подведем итоги Microsoft Fakes представляет собой Mock-фреймворк интегрированный в Visual Studio 2012 и позволяющий сильно упростить написание тестов на платформе .NET.

P.S. Если кому-то интересны какие-то особенности Fakes пишите комментарии постораюсь ответить.

Comments