Автор: Sergey Teplyakov

Все приведенные примеры можно найти на github.

Microsoft Fakes это очередной изоляционный фреймворк, который является логическим продолжением экспериментального проекта Microsoft Moles.

Все изоляционные фреймворки делятся на несколько категорий, в зависимости от времени генерации подделок и их типу:

По времени генерации:

  • генерация на лету во время исполнения тестов,
  • во время компиляции,

По типу генерируемых подделок:

  • на основе полиморфизма, что позволяет мокать лишь полиморфные методы,
  • на основе CLR Profiling API, что позволяет мокать невиртуальные методы, включая статические.

Так, например, Moq, Rhino Mocks, FakeItEasy и многие другие генерируют моки на лету и позволяют переопределять лишь виртуальные методы. Microsoft Fakes, с другой стороны, генерирует подделки во время компиляции, а также позволяет мокать невиртуальные и статические методы.

Это значит, что для того, чтобы получить фейковые классы, нужно выбрать в Solution Explorer нужную вам сборку и выбрать пункт меню Add Fakes Assembly, в результате чего для указанной сборки будет сгенерирована соответствующая сборка со стабами:

clip_image002

Аналогично Microsoft Moles, Microsoft Fakes поддерживает два вида подделок: Стабы (Stubs), которые позволяют переопределять виртуальные члены и Шимы (Shims), позволяющие подделывать невиртуальные и статические члены.

Стабы (Stubs)

В качестве примеров давайте рассмотрим создание стабов для интерфейса ILoggerDependency, аналогичный тому, что был рассмотрен в заметке Moq. Примеры использования:public interface ILoggerDependency
{
string GetCurrentDirectory(),
string GetDirectoryByLoggerName(string loggerName),
string DefaultLogger { get, }
event EventHandler RollingRequired,
T GetConfigValue<,T>,(),
}

Предположим, что этот интерфейс передается классу Logger в качестве аргументов конструктора и использует его в процессе функционирования. Однако для простоты тестов мы будем использовать стаб интерфейса ILoggerDependency напрямую, а не косвенно, через класс Logger.

При вызове Add Fakes Assemblies будет сгенерирован следующий стаб для интерфейса ILoggerDependency:public class StubILoggerDependency
: StubBase<,Samples.ILoggerDependency>,,
Samples.ILoggerDependency
{
//
// Summary:
// Sets the stub of ILoggerDependency.get_DefaultLogger()
public FakesDelegates.Func<,string>, DefaultLoggerGet,
//
// Summary:
// Sets the stub of ILoggerDependency.GetCurrentDirectory()
public FakesDelegates.Func<,string>, GetCurrentDirectory,
//
// Summary:
// Sets the stub of
// ILoggerDependency.GetDirectoryByLoggerName(String loggerName)
public FakesDelegates.Func<,string, string>,
GetDirectoryByLoggerNameString,
public EventHandler RollingRequiredEvent,
//
// Summary:
// Sets stubs of GetConfigValue<,T>,()
public void GetConfigValueOf1<,T>,(FakesDelegates.Func<,T>, stub),
}

Ниже будет представлен набор примеров задания поведения стабов и тесты, проверяющие ожидаемое поведение.

1. Стаб метода GetCurrentDirectoryvar stub = new StubILoggerDependency(),
stub
.GetCurrentDirectory = () =>, 'D:\\Temp',
// StubILoggerDependency реализует интерфейс ILoggerDependency
ILoggerDependency loggerDependency = stub,
var currentDirectory = loggerDependency.GetCurrentDirectory(),
Assert.That(currentDirectory, Is.EqualTo('D:\\Temp'
)),2. Стаб метода GetCurrentDirectoryByLoggerName, всегда возвращающий один результатvar stub = new StubILoggerDependency(),
// При любом аргументе стаб вернет 'C:\\Foo'.
stub.GetDirectoryByLoggerNameString = (string logger) =>, 'C:\\Foo',
ILoggerDependency loggerDependency = stub,
string directory = loggerDependency.GetDirectoryByLoggerName('Any Value'),
Assert.That(directory, Is.EqualTo('C:\\Foo'
)),

3. Стаб метода GetCurrentDirectoryByLoggerName, возвращающий результат в зависимости от аргументаvar stub = new StubILoggerDependency(),
// Данный стаб аналогичен следующему коду:
// public string GetDirectoryByLoggername(string s) { return 'C:\\' + s, }
stub.GetDirectoryByLoggerNameString = (string logger) =>, 'C:\\' + logger,
ILoggerDependency loggerDependency = stub,
// Это параметризованный тест, loggerName является аргументом теста
string directory = loggerDependency.GetDirectoryByLoggerName(loggerName),
Assert.That(directory, Is.EqualTo('C:\\' + loggerName
)),

4. Стаб свойства DefaultLoggervar stub = new StubILoggerDependency(),
stub
.DefaultLoggerGet = () =>, 'DefaultLogger',
ILoggerDependency loggerDependency = stub,
string defaultLogger = loggerDependency.DefaultLogger,
Assert.That(defaultLogger, Is.EqualTo('DefaultLogger'
)),

5. Стаб события RollingRequiredEventvar stub = new StubILoggerDependency(),
ILoggerDependency loggerDependency = stub,
bool eventOccurred = false
,
loggerDependency
.RollingRequired += (sender, args) =>, eventOccurred = true,
// Вызываем событие вручную
stub.RollingRequiredEvent(this, EventArgs.Empty),
Assert.IsTrue(eventOccurred, 'Event should be raised!'
),

6. Стаб обобщенного метода GetConfigValue<,T>,var stub = new StubILoggerDependency(),
stub
.GetConfigValueOf1<,string>,(() =>, 'default'),
ILoggerDependency loggerDependency = stub,
var str = loggerDependency.GetConfigValue<,string>,(),
Assert.That(str, Is.EqualTo('default'
)),

Шимы (Shims)

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

ПРИМЕЧАНИЕ
Нужно четко понимать, что использование подобных инструментов может привести к тому, что мы сможем покрыть тестами любой код, не важно, насколько он для этого предназначен и насколько его дизайн хорош. Мы можем замокать DateTime.Now, статические методы чтения данных из базы данных и т.п. Но нужно четко понимать, что это может привести к хрупким тестам и отбить желание исправить проблемы в дизайне, которые бы позволили покрыть код тестами без таких ухищрений.
Тем не менее, когда речь касается унаследованного кода, то такие инструменты могут помочь на первых этапах: мы можем покрыть нетестируемый код тестами с помощью Shims, а уже потом начать его рефакторинг для улучшения дизайна, используя существующий набор тестов, как страховку.

В качестве примера давайте рассмотрим конкретную зависимость класса Logger SealedLoggerDependency, замокать которую за счет полиморфизма невозможно:/// <,summary>,
/// Класс аналогичен интерфейсу
ILoggerDependency за исключением того,
// что это закрытый класс
.
///
<,/summary>,
public sealed class SealedLoggerDependency
{
public string GetCurrentDirectory()
{
throw new NotImplementedException(),
}
public string GetDirectoryByLoggerName(string loggerName)
{
throw new NotImplementedException(),
}
public string DefaultLogger
{
get { throw new NotImplementedException(), }
}
public event EventHandler RollingRequired,
public T GetConfigValue<,T>,()
{
throw new NotImplementedException(),
}
public static string GetDefaultDirectory()
{
throw new NotImplementedException(),
}
}

Класс SealedLoggerDependency аналогичен интерфейсу ILoggerDependency за исключением того, что класс является закрытым (sealed) и вдобавок, содержит статический метод GetDefaultDirectory.

Для класса SealedLoggerDependency Microsoft Fakes сгенерирует ShimSealedLoggerDependency, очень похожий на класс StubILoggerDependency с небольшими отличиями, которые будут понятны по примерам. Главное же отличие шимов от стабов заключается в том, что шимы нужно запускать в специальном контексте, который инициализирует всю необходимую магию.

1. Стаб метода GetCurrentDirectoryusing (ShimsContext.Create())
{
var shim = new ShimSealedLoggerDependency(),
shim.GetCurrentDirectory = () =>, 'D:\\Temp',
// Существует неявное преобразование типа от Shim-а к объекту
SealedLoggerDependency loggerDependency = shim,
var currentDirectory = loggerDependency.GetCurrentDirectory(),
Assert.That(currentDirectory, Is.EqualTo('D:\\Temp')),
}

2. Стаб метода GetCurrentDirectory, всегда возвращающий один результат для всех экземпляров SealedLoggerDependencyusing (ShimsContext.Create())
{
ShimSealedLoggerDependency.AllInstances.GetCurrentDirectory =
(SealedLoggerDependency d) =>, 'D:\\Temp',
var loggerDependency = new SealedLoggerDependency(),
var currentDirectory = loggerDependency.GetCurrentDirectory(),
Assert.That(currentDirectory, Is.EqualTo('D:\\Temp')),
}

3. Стаб метода GetCurrentDirectoryByLoggerName, возвращающий результат в зависимости от аргументаusing (ShimsContext.Create())
{
var shim = new ShimSealedLoggerDependency(),
shim.GetDirectoryByLoggerNameString = (string logger) =>,
logger + 'C:\\Foo',
var loggerDependency = shim.Instance,
// loggerName это аргумент параметризованного теста
string directory = loggerDependency.GetDirectoryByLoggerName(loggerName),
Assert.That(directory, Is.EqualTo(loggerName + 'C:\\Foo')),
}

4. Стаб свойства DefaultLoggerusing (ShimsContext.Create())
{
var shim = new ShimSealedLoggerDependency(),
shim.DefaultLoggerGet = () =>, 'DefaultLogger',
var loggerDependency = shim.Instance,
string defaultLogger = loggerDependency.DefaultLogger,
Assert.That(defaultLogger, Is.EqualTo('DefaultLogger')),
}

5. Стаб события RollingRequiredEventusing (ShimsContext.Create())
{
var shim = new ShimSealedLoggerDependency(),
var loggerDependency = shim.Instance,
// Мы не можем переопределить событие напрямую,
// а может лишь переопределить подписку и отписку
EventHandler backingDelegate = null,
shim.RollingRequiredAddEventHandler = handler =>,
backingDelegate += handler,
bool eventOccurred = false,
loggerDependency.RollingRequired += (sender, args) =>,
eventOccurred = true,
// Поскольку backingDelegate содержит все наши подписки
// то мы можем сэмулировать генерацию события путем
// запуска этого делегата!
backingDelegate(this, EventArgs.Empty),
Assert.IsTrue(eventOccurred, 'Event should be raised!'),
}

6. Стаб обобщенного метода GetConfigValue<,T>,using (ShimsContext.Create())
{
var shim = new ShimSealedLoggerDependency(),
shim.GetConfigValueOf1(() =>, 'default'),
var loggerDependency = shim.Instance,
var str = loggerDependency.GetConfigValue<,string>,(),
Assert.That(str, Is.EqualTo('default')),
}

7. Стаб статического метода GetDefaultDirectoryusing (ShimsContext.Create())
{
ShimSealedLoggerDependency.GetDefaultDirectory = () =>, 'C:\\Windows',
var defaultDirectory = SealedLoggerDependency.GetDefaultDirectory(),
Console.WriteLine('Default directory is '{0}'', defaultDirectory),
Assert.That(defaultDirectory, Is.EqualTo('C:\\Windows')),
}

8. Вызов оригинального метода

Давайте изменим класс SealedLoggerDependency, чтобы исходная версия метода GetDefaultDirectory увеличивал счетчик GetDefaultDirectoryCallsCount:public static int GetDefaultDirectoryCallsCount,
public static string
GetDefaultDirectory()
{
GetDefaultDirectoryCallsCount++,
return GetDefaultDirectoryCallsCount.ToString(),
}

Теперь мы можем написать тест, вызывающий базовую версию метода:using (ShimsContext.Create())
{
int initialCallsCount =
SealedLoggerDependency.GetDefaultDirectoryCallsCount,
ShimSealedLoggerDependency.GetDefaultDirectory = () =>, 'C:\\Windows',
// Вызываем подмененную версию
SealedLoggerDependency.GetDefaultDirectory(),
Assert.That(SealedLoggerDependency.GetDefaultDirectoryCallsCount,
Is.EqualTo(initialCallsCount)),
// Вызываем оригинальную версию
var defaultValue = ShimsContext.ExecuteWithoutShims(
() =>, SealedLoggerDependency.GetDefaultDirectory()),
// Вызов оригинальной версии должен увеличить количество вызовов
Assert.That(SealedLoggerDependency.GetDefaultDirectoryCallsCount,
Is.EqualTo(initialCallsCount + 1)),
}

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

ПРИМЕЧАНИЕ
На данный момент использовать шимы можно только со встроенным тест-раннером, а попытка запустить их с помощью Resharper приводит к ошибке.

Не сложно заметить, что во всех приведенных примерах подделки эмулировали требуемое возвращаемые значения и использовались для проверки граничных условий тестируемых классов. Помимо эмуляции состояния иногда бывает нужным убедиться в том, что тестируемый класс при определенных условиях выполняет некоторые действия. Это называется проверкой поведения и здесь Microsoft Fakes практически ничего предложить не может.

В следующий раз мы рассмотрим, как проверить поведение с помощью Microsoft Fakes.

Дополнительные ссылки
  • Moq. Примеры использования
  • Microsoft Moles
  • Стабы и моки

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).