понедельник, 20 января 2014 г.

Microsoft Fakes. Тестирование поведения

DISCLAIMER: исходный код Verification Fakes можно найти на github, а скомпилированная версия доступна через nuget.org/packages/VerificationFakes.

В прошлой заметке мы рассмотрели примеры использования Microsoft Fakes для создания стабов – заглушек, возвращающих требуемые данные. Однако в некоторых случаях нам все же нужно проверить, что в определенных условиях тестируемый класс обращается к некоторой зависимости. Такой вид тестирования называется тестированием поведением и именно такой вид «подделок» каркасом Microsoft Fakes практически не поддерживается.

Все что можно сделать в Microsoft Fakes – это установить наблюдатель с помощью StubBase.InstanceObserver, но пользоваться таким подходом вручную довольно сложно.

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

public interface ILogWriter
{
   
void Write(string
message);

   
void Write(int
value);
}

public class Logger
{
   
private readonly ILogWriter
_logWriter;

   
public Logger(ILogWriter
logWriter)
    {
        _logWriter
=
logWriter;
    }

   
public void Write(string
message)
    {
        _logWriter
.
Write(message);
    }

   
public void Write(int
value)
    {
        _logWriter
.Write(value);
    }
}

Так, имея заглушку интерфейса ILogWriter, сгенерированную Microsoft Fakes, мы можем написать тест, который будет проверять, что Logger при вызове метода Write вызывает соответствующий метод своей зависимости.

Как уже говорилось выше, для этого вначале нужно реализовать интерфейс IStubObserver и передать его сгенерированному стабу:

class CustomObserver : IStubObserver
{
   
public string
CalledMethodName;
   
public object
CalledMethodArgument;

   
public void Enter(Type stubbedType, Delegate stubCall, object
arg1)
    {
        CalledMethodName
= stubCall.Method.
Name;
        CalledMethodArgument
=
arg1;
    }

   
// Остальные перегрузки методы Enter опущены
}

[Test]
public void
Logger_Write_Calls_For_Enter_Method()
{
   
// Arrange
    var stub = new StubILogWriter
();
   
var customObserver = new CustomObserver
();
   
// Providing custom observer to the stub
    stub.InstanceObserver =
customObserver;
   
var logger = new Logger
(stub);

   
// Act
    logger.Write("Message"
);

   
// Assert
    Assert.That(customObserver.
CalledMethodName,
       
Is.StringContaining("ILogWriter.Write"
));
   
Assert.That(customObserver.CalledMethodArgument, Is.EqualTo("Message"));
}

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

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

Verification Fakes – это легковесная оболочка над Microsoft Fakes, которая предоставляет интерфейс для тестирования поведения, аналогичный библиотеке Moq. Поэтому если у вас есть опыт использования Moq, то примеры Verification Fakes будут очень знакомыми.

Тогда, если мы сгенерируем фейки для этих интерфейсов, то сможем тестировать поведение с помощью Verification Fakes. Для этого из сгенерированного стаба нужно получить экземпляр Mock<T> с помощью методов расширения AsMock или AsStrictMock:

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

var stub = new StubILogWriter();
Mock<ILogWriter> mock = stub.AsMock();
var logger = new Logger
(stub);

// Вызываем метод тестируемого класса


logger.Write("Hello, logger!"
);
           


// Проверяем, что был вызван метод Write с любым аргументом

mock.Verify(lw => lw.Write(It.IsAny<string>()));

Теперь, если мы закомментируем строку с вызовом метода logger.Write, то метод mock.Verify сгенерирует исключение VerificationException с соответствующим сообщением об ошибке:

VerificationFakes.VerificationException: Expected invocation on the mock 1 times, but was 0 times.
Expected call: lw => lw.Write(It.IsAny<string>())
Actual calls:
No invocations performed.

2. Метод LogWriter.Write вызывается с заданным аргументом:

// Вызываем метод тестируемого класса
logger.Write("Hello, logger!"
);


// Проверяем, что вызван метод с конкретными аргументами

mock.Verify(lw => lw.Write("Hello, logger!"));

В этом случае, если будет вызван метод Write с другим аргументом мы получим следующую ошибку:

Expected invocation on the mock 1 times, but was 0 times.
Expected call: lw => lw.Write("Hello, logger!")
Actual calls:
VerificationFakes.Samples.ILogWriter.Write("Some other message")

3. Метод LogWriter.Write вызывается в точности один раз:

// Вызываем метод тестируемого класса
logger.Write("Hello, logger!"
);


// Проверяем, что метод вызван в точности один раз

mock.Verify(lw => lw.Write(It.IsAny<string
>()),
   
Times.Once());

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

4. Метод LogWriter.Write(int) не вызывался при вызове Logger.Write(string):

// Вызываем метод тестируемого класса
logger.Write("Hello, logger"
);

// Проверяем, что метод Write(int) не был вызван
mock.Verify(lw => lw.Write(It.IsAny<int>()), Times.Never());

5. Метод LogWriter.Write(int) вызван с аргументом в указанном диапазоне:

// Вызываем метод тестируемого класса
logger.Write(42
);

// Проверяем, что аргумент метод Write(int) находится в указанном диапазоне
mock.Verify(lw => lw.Write(It.IsInRange(40, 50)));

6. Использование методов Setup и Verify для указания нескольких ожидаемых вызовов:

var stub = new StubILogWriter();
var mock = stub.
AsMock();
mock
.Setup(lw => lw.Write(It.IsAny<string
>()));
mock
.Setup(lw => lw.Write(It.IsAny<int
>()));

var logger = new Logger(mock.
Object);

// Вызываем два метода тестируемого класса
logger.Write("Hello, logger!"
);
logger
.Write(42
);

// Проверяем, что были вызваны все методы,
// указанные путем вызова метода Setup

mock.Verify();

7. Пример использование строгого мока:

var stub = new StubILogWriter();
var mock = stub.
AsStrictMock();
mock
.Setup(lw => lw.Write("Foo"
));

// Вызываем метод тестируемого класса
var logger = new Logger(mock.
Object);

logger
.Write("Foo");

В этом случае, при попытке вызова любого метода LogWriter помимо метода Write с указанным аргументом, мы получим VerificationException. Так, в случае вызова метода logger.Write(42) мы получим следующее сообщение:

Following invocations failed with mock behavior Strict:
VerificationFakes.Samples.ILogWriter.Write(42)
Expected invocations:
lw => lw.Write("Foo"), 1 times.

Заключение

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

Тем не менее, иногда такие тесты вполне полезны, особенно если они не завязаны на точное значение аргументов и проверяют действительно ключевое поведение тестируемых классов.

Для меня же Verification Fakes был весьма интересным домашним проектом, и, возможно, он будет интересен тем двоим программистам, которые используют Microsoft Fakes в своих проектахJ

Дополнительные ссылки

4 комментария:

  1. Поправьте опечаточки, пожалуйста:
    В заголовке статьи: Тестирование поведениЯ
    Так, имЕя заглушку интерфейса ILogWriter

    ОтветитьУдалить
  2. Спасибо за статью

    Я использую способ попроще, кстати, он описан у Вас в предыдущей статье
    Цитирую Ваш код:
    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!");

    То есть Вы объявляете переменную eventOccurred, а в конце проверяете чтобы она была true
    Такой же подход можно использовать и в замоканых методах стаба. И еще внутри проверять соответствие переданных параметров ожидаемым (sender, args в Вашем случае)
    А еще вместо булевой переменной можно использовать счетчик вызовов или единый логгер последовательности вызовов. Тут уж как фантазия позволит :)
    ИМХО это гибче чем IStubObserver

    ОтветитьУдалить
    Ответы
    1. Спасибо за дополнение!
      Я и правда когда-то использовал аналогичный подход, но мне он показался все же более "императивным" по сравнению с "декларативным" подходом на основе велосипеда.

      Хотя внедрение подобного велосипеда само по себе дело довольно непростое.

      Удалить