Примеры использования Moq

    Moq – это простой и легковесный изоляционный фреймврк (Isolation Framework), который построен на основе анонимных методов и деревьев выражений. Для создания моков он использует кодогенерацию, поэтому позволяет «мокать» интерфейсы, виртуальные методы (и даже защищенные методы) и не позволяет «мокать» невиртуальные и статические методы.

    ПРИМЕЧАНИЕ
    На рынке существует лишь два фрейморка, позволяющих «мокать» все, что угодно. Это TypeMockIsolator и Microsoft Fakes, доступные в Visual Studio 2012 (ранее известные под названием Microsoft Moles). Эти фреймворки, в отличие от Moq, используют не кодогенерацию, а CLR Profiling API, что позволяет вклиниться практически в любой метод и создать моки/стабы даже для статических, невиртуальных или закрытых методов.

    В Moq нет разделения между «стабами» (stubs) и «моками» (mocks) или, более формально, нет разделения на верификацию состояния и верификацию поведения. И хотя в большинстве случаев различия между стабами и моками не так уж и важны, а иногда одна и та же заглушка выполняет обе роли, мы будем рассматривать примеры от простых к сложным, поэтому вначале рассмотрим примеры проверки состояния, а уже потом перейдем к проверке поведения.

    Проверка состояния (state verification)


    В качестве примера мы будем рассматривать набор юнит тестов для следующего интерфейса:

    public interface ILoggerDependency 
    { 
        string GetCurrentDirectory(); 
        string GetDirectoryByLoggerName(string loggerName); 
        string DefaultLogger { get; } 
    } 
    


    1. Стаб метода GetCurrentDirectory:

    // Mock.Of возвращает саму зависимость (прокси-объект), а не мок-объект.
    // Следующий код означает, что при вызове GetCurrentDirectory()
    // мы получим "D:\\Temp"
    ILoggerDependency loggerDependency =
        Mock.Of<ILoggerDependency>(d => d.GetCurrentDirectory() == "D:\\Temp");
    var currentDirectory = loggerDependency.GetCurrentDirectory();
     
    Assert.That(currentDirectory, Is.EqualTo("D:\\Temp"));
    


    2. Стаб метода GetDirectoryByLoggerName, всегда возвращающий один и тот же результат:

    // Для любого аргумента метода GetDirectoryByLoggerName вернуть "C:\\Foo".
    ILoggerDependency loggerDependency = Mock.Of<ILoggerDependency>(
        ld => ld.GetDirectoryByLoggerName(It.IsAny<string>()) == "C:\\Foo");
     
    string directory = loggerDependency.GetDirectoryByLoggerName("anything");
     
    Assert.That(directory, Is.EqualTo("C:\\Foo"));
    


    3. Стаб метода GetDirrectoryByLoggerName, возвращающий результат в зависимости от аргумента:

    // Инициализируем заглушку таким образом, чтобы возвращаемое значение
    // метода GetDirrectoryByLoggerName зависело от аргумента метода.
    // Код аналогичен заглушке вида:
    // public string GetDirectoryByLoggername(string s) { return "C:\\" + s; }
    Mock<ILoggerDependency> stub = new Mock<ILoggerDependency>();
     
    stub.Setup(ld => ld.GetDirectoryByLoggerName(It.IsAny<string>()))
        .Returns<string>(name => "C:\\" + name);
     
    string loggerName = "SomeLogger";
    ILoggerDependency logger = stub.Object;
    string directory = logger.GetDirectoryByLoggerName(loggerName);
     
    Assert.That(directory, Is.EqualTo("C:\\" + loggerName));
    


    4. Стаб свойства DefaultLogger:

    // Свойство DefaultLogger нашей заглушки будет возвращать указанное значение
    ILoggerDependency logger = Mock.Of<ILoggerDependency>(
        d => d.DefaultLogger == "DefaultLogger");
     
    string defaultLogger = logger.DefaultLogger;
     
    Assert.That(defaultLogger, Is.EqualTo("DefaultLogger"));
    


    5. Задание поведения нескольких методов одним выражением с помощью “moq functional specification” (появился в Moq v4):

    // Объединяем заглушки разных методов с помощью логического «И»
    ILoggerDependency logger =
        Mock.Of<ILoggerDependency>(
            d => d.GetCurrentDirectory() == "D:\\Temp" &&
                    d.DefaultLogger == "DefaultLogger" &&
                    d.GetDirectoryByLoggerName(It.IsAny<string>()) == "C:\\Temp");
     
    Assert.That(logger.GetCurrentDirectory(), Is.EqualTo("D:\\Temp"));
    Assert.That(logger.DefaultLogger, Is.EqualTo("DefaultLogger"));
    Assert.That(logger.GetDirectoryByLoggerName("CustomLogger"), Is.EqualTo("C:\\Temp"));
    


    6. Задание поведение нескольких методов с помощью вызова методов Setup («старый» v3 синтаксис):

    var stub = new Mock<ILoggerDependency>();
    stub.Setup(ld => ld.GetCurrentDirectory()).Returns("D:\\Temp");
    stub.Setup(ld => ld.GetDirectoryByLoggerName(It.IsAny<string>())).Returns("C:\\Temp");
    stub.SetupGet(ld => ld.DefaultLogger).Returns("DefaultLogger");
     
    ILoggerDependency logger = stub.Object;
     
    Assert.That(logger.GetCurrentDirectory(), Is.EqualTo("D:\\Temp"));
    Assert.That(logger.DefaultLogger, Is.EqualTo("DefaultLogger"));
    Assert.That(logger.GetDirectoryByLoggerName("CustomLogger"), Is.EqualTo("C:\\Temp"));
    


    ПРИМЕЧАНИЕ
    Как я уже упоминал, в Moq нет разделения между моками и стабами, однако нам с вами будет значительно проще различать два синтаксиса инициализации заглушек. Так, «moq functional specification» синтаксис может использоваться только для тестирования состояния (т.е. для стабов) и не может применяться для задания поведения. Инициализация же заглушек методом Setup может быть, во-первых, более многословной, а во-вторых, при ее использовании не совсем понятно, собираемся ли мы проверять поведение или состояние.

    Проверка поведения (behavior verification)


    Для тестирования поведения будет использоваться следующий класс и интерфейс:

    public interface ILogWriter
    {
        string GetLogger();
        void SetLogger(string logger);
        void Write(string message);
    }
    public class Logger
    {
        private readonly ILogWriter _logWriter;
     
        public Logger(ILogWriter logWriter)
        {
            _logWriter = logWriter;
        }
     
        public void WriteLine(string message)
        {
            _logWriter.Write(message);
        }
    }
    


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

    var mock = new Mock<ILogWriter>();
    var logger = new Logger(mock.Object);
     
    logger.WriteLine("Hello, logger!");
     
    // Проверяем, что вызвался метод Write нашего мока с любым аргументом
    mock.Verify(lw => lw.Write(It.IsAny<string>()));
    


    2. Проверка вызова метода ILogWriter.Write с заданным аргументами:

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


    3. Проверка того, что метод ILogWriter.Write вызвался в точности один раз (ни больше, ни меньше):

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


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

    4. Проверка поведения с помощью метода Verify (может быть удобной, когда нужно проверить несколько допущений):

    var mock = new Mock<ILogWriter>();
    mock.Setup(lw => lw.Write(It.IsAny<string>()));
     
    var logger = new Logger(mock.Object);
    logger.WriteLine("Hello, logger!");
     
    // Мы не передаем методу Verify никаких дополнительных параметров.
    // Это значит, что будут использоваться ожидания установленные
    // с помощью mock.Setup
    mock.Verify();
    


    5. Проверка нескольких вызовов с помощью метода Verify().

    В некоторых случаях неудобно использовать несколько методов Verify для проверки нескольких вызовов. Вместо этого можно создать мок-объект и задать ожидаемое поведение с помощью методов Setup и проверять все эти допущения путем вызова одного метода Verify(). Такая техника может быть удобной для повторного использования мок-объектов, создаваемых в методе Setup теста.

    var mock = new Mock<ILogWriter>();
    mock.Setup(lw => lw.Write(It.IsAny<string>()));
    mock.Setup(lw => lw.SetLogger(It.IsAny<string>()));
     
    var logger = new Logger(mock.Object);
    logger.WriteLine("Hello, logger!");
     
    mock.Verify();
    


    Отступление от темы. Strict vs Loose модели



    Moq поддерживает две модели проверки поведения: строгую (strict) и свободную (loose). По умолчанию используется свободная модель проверок, которая заключается в том, что тестируемый класс (Class Under Test, CUT), во время выполнения действия (в секции Act) может вызывать какие угодно методы наших зависимостей и мы не обязаны указывать их все.

    Так, в предыдущем примере метод logger.WriteLine вызывает два метода интерфейса ILogWriter: метод Write и SetLogger. При использовании MockBehavior.Strict метод Verify завершится неудачно, если мы не укажем явно, какие точно методы зависимости будут вызваны:

    var mock = new Mock<ILogWriter>(MockBehavior.Strict);
    // Если закомментировать одну из следующих строк, то
    // метод mock.Verify() завершится с исключением
    mock.Setup(lw => lw.Write(It.IsAny<string>()));
    mock.Setup(lw => lw.SetLogger(It.IsAny<string>()));
     
    var logger = new Logger(mock.Object);
    logger.WriteLine("Hello, logger!");
     
    mock.Verify();
    


    Использование MockRepository



    Класс MockRepository предоставляет еще один синтаксис для создания стабов и, что самое главное, позволяет хранить несколько мок-объектов и проверять более комплексное поведение путем вызова одного метода.

    1. Использование MockRepository.Of для создания стабов.
    Данный синтаксис аналогичен использованию Mock.Of, однако позволяет задавать поведение разных методов не через оператор &&, а путем использования нескольких методов Where:

    var repository = new MockRepository(MockBehavior.Default);
    ILoggerDependency logger = repository.Of<ILoggerDependency>()
        .Where(ld => ld.DefaultLogger == "DefaultLogger")
        .Where(ld => ld.GetCurrentDirectory() == "D:\\Temp")
        .Where(ld => ld.GetDirectoryByLoggerName(It.IsAny<string>()) == "C:\\Temp")
        .First();
     
    Assert.That(logger.GetCurrentDirectory(), Is.EqualTo("D:\\Temp"));
    Assert.That(logger.DefaultLogger, Is.EqualTo("DefaultLogger"));
    Assert.That(logger.GetDirectoryByLoggerName("CustomLogger"), Is.EqualTo("C:\\Temp"));
    


    2. Использование MockRepository для задания поведения нескольких мок-объектов.
    Предположим, у нас есть более сложный класс SmartLogger, которому требуется две зависимости: ILogWriter и ILogMailer. Наш тестируемый класс при вызове его метода Write должен вызвать методы двух зависимостей:

    var repo = new MockRepository(MockBehavior.Default);
    var logWriterMock = repo.Create<ILogWriter>();
    logWriterMock.Setup(lw => lw.Write(It.IsAny<string>()));
     
    var logMailerMock = repo.Create<ILogMailer>();
    logMailerMock.Setup(lm => lm.Send(It.IsAny<MailMessage>()));
     
    var smartLogger = new SmartLogger(logWriterMock.Object, logMailerMock.Object);
     
    smartLogger.WriteLine("Hello, Logger");
     
    repo.Verify();
    


    Другие техники



    В некоторых случаях бывает полезным получить сам мок-объект по интерфейсу (получить Mock<ISomething> по интерфейсу ISomething). Например, функциональный синтаксис инициализации заглушек возвращает не мок-объект, а сразу требуемый интерфейс. Это бывает удобным для тестирования пары простых методов, но неудобным, если понадобится еще и проверить поведение, или задать метод, возвращающий разные результаты для разных параметров. Так что иногда бывает удобно использовать LINQ-based синтаксис для одной части методов и использовать методы Setup – для другой:

    ILoggerDependency logger = Mock.Of<ILoggerDependency>(
        ld => ld.GetCurrentDirectory() == "D:\\Temp"
            && ld.DefaultLogger == "DefaultLogger");
     
    // Задаем более сложное поведение метода GetDirectoryByLoggerName
    // для возвращения разных результатов, в зависимости от аргумента
    Mock.Get(logger)
        .Setup(ld => ld.GetDirectoryByLoggerName(It.IsAny<string>()))
        .Returns<string>(loggerName => "C:\\" + loggerName);
     
    Assert.That(logger.GetCurrentDirectory(), Is.EqualTo("D:\\Temp"));
    Assert.That(logger.DefaultLogger, Is.EqualTo("DefaultLogger"));
    Assert.That(logger.GetDirectoryByLoggerName("Foo"), Is.EqualTo("C:\\Foo"));
    Assert.That(logger.GetDirectoryByLoggerName("Boo"), Is.EqualTo("C:\\Boo"));
    


    Помимо этого Moq позволяет проверять поведение защищенных методов, тестировать события и содержит некоторые другие возможности.

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



    Примеры на github
    Моки и стабы
    Microsoft Moles
    • +21
    • 89k
    • 3
    Share post

    Similar posts

    Comments 3

      +1
      Отличная и простая библиотека без лишней мишуры. Funq (внедрение зависимостей) от того же автора такая же.
        –1
        Статья безусловно интересная, но не хватает ссылки на сайт авторов Moq
          0
          Точнее на сайт проекта

        Only users with full accounts can post comments. Log in, please.