Все приведенные примеры можно найти на github.
Microsoft Fakes – это очередной изоляционный фреймворк, который является логическим продолжением экспериментального проекта Microsoft Moles.
Все изоляционные фреймворки делятся на несколько категорий, в зависимости от времени генерации «подделок» и их типу:
По времени генерации:
- генерация на лету во время исполнения тестов;
- во время компиляции;
По типу генерируемых «подделок»:
- на основе полиморфизма, что позволяет «мокать» лишь полиморфные методы;
- на основе CLR Profiling API, что позволяет мокать невиртуальные методы, включая статические.
Так, например, Moq, Rhino Mocks, FakeItEasy и многие другие генерируют «моки» на лету и позволяют переопределять лишь виртуальные методы. Microsoft Fakes, с другой стороны, генерирует «подделки» во время компиляции, а также позволяет «мокать» невиртуальные и статические методы.
Это значит, что для того, чтобы получить фейковые классы, нужно выбрать в “Solution Explorer” нужную вам сборку и выбрать пункт меню “Add Fakes Assembly”, в результате чего для указанной сборки будет сгенерирована соответствующая сборка со стабами:
Аналогично 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. Стаб метода GetCurrentDirectory
var 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. Стаб свойства DefaultLogger
var stub = new StubILoggerDependency();
stub.DefaultLoggerGet = () => "DefaultLogger";
ILoggerDependency loggerDependency = stub;
string defaultLogger = loggerDependency.DefaultLogger;
Assert.That(defaultLogger, Is.EqualTo("DefaultLogger"));
5. Стаб события RollingRequiredEvent
var 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. Стаб метода GetCurrentDirectory
using (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, всегда возвращающий один результат для всех экземпляров SealedLoggerDependency
using (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. Стаб свойства DefaultLogger
using (ShimsContext.Create())
{
var shim = new ShimSealedLoggerDependency();
shim.DefaultLoggerGet = () => "DefaultLogger";
var loggerDependency = shim.Instance;
string defaultLogger = loggerDependency.DefaultLogger;
Assert.That(defaultLogger, Is.EqualTo("DefaultLogger"));
}
5. Стаб события RollingRequiredEvent
using (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. Стаб статического метода GetDefaultDirectory
using (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.
Комментариев нет:
Отправить комментарий