Мы кратко рассмотрели создание тестовых макетов в предыдущей части этой серии. В то время основное внимание уделялось концепции насмешек, и чтобы не отклоняться в сторону, мы использовали один из самых простых способов создания и настройки нашей насмешки. Но когда мы приступаем к тестированию более сложной логики реального мира, нам могут понадобиться дополнительные параметры конфигурации. На этой неделе мы рассмотрим два варианта синтаксиса, которые мы можем использовать для настройки моков с помощью Moq, а также их плюсы и минусы.

Современный подход к объявлению и настройке макетов

Когда мы ранее вводили насмешки, мы создавали имитации с помощью LINQ to Mocks. Этот (относительно) новый синтаксис должен быть довольно интуитивным, если вы привыкли писать запросы LINQ с использованием синтаксиса метода, то есть с методами расширения, такими как First, Single и Where. Чтобы освежить наши воспоминания, вот как мы объявили наш макет.

var multiplicationService = Mock.Of<IMultiplicationService>(s =>
    s.Square(3) == 9 &&
    s.Square(4) == 16);

При использовании LINQ to Mocks возвращаемое значение (из Mock.Of) является имитируемым объектом. В этом примере это IMultiplicationService, который можно использовать в качестве аргумента метода или установить в качестве значения свойства.

Свободная альтернатива LINQ to Mocks

Мы также можем создавать и настраивать макеты, используя свободный синтаксис. Чтобы добиться того же результата, что и в нашем предыдущем примере LINQ to Mocks, мы можем использовать следующий код.

var multiplicationService = new Mock<IMultiplicationService>();
            
multiplicationService
    .Setup(s => s.Square(3))
    .Returns(9);

multiplicationService
    .Setup(s => s.Square(4))
    .Returns(16);

В этом случае multiplicationService — это объект, представляющий макет, а не имитируемый объект. Мы можем легко получить IMultiplicationService для использования в наших тестах из свойства макета Object. Здесь это будет multiplicationService.Object.

Хотя он более подробный, чем LINQ to Mocks, все же легко увидеть, что происходит. Но остается один вопрос: зачем нам писать больше кода для достижения того же результата?

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

Когда аргументы просто не имеют значения

В предыдущих примерах аргумент в пользу нашего имитируемого метода имел значение; возвращаемое значение зависело от предоставленных данных:

  • 9 было возвращено, когда Square был вызван с 3.
  • 16 было возвращено, когда Square был вызван с 4.

В некоторых тестовых контекстах аргументы метода макета не имеют значения — нам просто нужно, чтобы он возвращал что-то для использования в тесте. В этих ситуациях мы можем использовать It.IsAny<T>(), указав общий тип, соответствующий типу параметра. Например, представьте, что у нас есть следующий код.

public class MyObject
{
    public int Id { get; set; }
}

public interface IObjectRepository
{
    MyObject GetObject(int id);
}

Предположим, у нас есть тест, в котором IObjectRepository должен возвращать MyObject независимо от переданного ему идентификатора. Мы можем настроить mock для этого с помощью следующего кода:

var repository = Mock.Of<IObjectRepository>(r =>
    r.GetObject(It.IsAny<int>()) == new MyObject());

Мы используем It.IsAny<int>() вместо фактического целого числа при настройке макета. Результатом является IObjectRepository, который всегда возвращает указанное MyObject независимо от значения id.

Преимущества свободного синтаксиса

Возвращаясь к нашему предыдущему вопросу: почему мы можем написать больше кода для того же результата? Ответ заключается в том, что результаты немного отличаются, и у нас есть больший контроль с плавным синтаксисом. В следующем тесте мы получаем MyObject дважды. Затем мы проверяем, относятся ли они к разным экземплярам объекта.

[Test]
public void ReturnedObjectIsNewInstance()
{
    // Arrange

    var repository = Mock.Of<IObjectRepository>(r =>
        r.GetObject(It.IsAny<int>()) == new MyObject());

    // Act

    var obj1 = repository.GetObject(1);
    var obj2 = repository.GetObject(2);

    // Assert

    Assert.IsTrue(obj1 != obj2);
}

Может показаться сюрпризом, что этот тест не пройден:

Expected: True
But was:  False

Вызов метода, созданного с помощью LINQ to Mocks, возвращает одно и то же значение при последовательных вызовах. Хотя тестирование макета — это не то, что мы будем делать в реальном проекте, результат имеет значение, если логика в тесте вызывает метод макета более одного раза. Если для макета важно возвращать новый экземпляр при каждом вызове, мы можем рассмотреть возможность использования свободного синтаксиса:

[Test]
public void ReturnedObjectIsNewInstance()
{
    // Arrange

    var repositoryMock = new Mock<IObjectRepository>();
        
    repositoryMock
        .Setup(r => r.GetObject(It.IsAny<int>()))
        .Returns<int>(i => new MyObject());

    var repository = repositoryMock.Object;

    // Act

    var obj1 = repository.GetObject(1);
    var obj2 = repository.GetObject(2);

    // Assert

    Assert.IsTrue(obj1 != obj2);
}

Запуск этого теста проходит.

Краткое содержание

Moq позволяет создавать макеты с одним из двух синтаксисов, каждый из которых имеет свои преимущества. LINQ to Mocks современный, лаконичный и знакомый. Свободный стиль предлагает больше контроля.

При использовании LINQ to Mocks вы можете легко объявить и настроить макет с помощью кода, аналогичного написанию запроса LINQ с использованием синтаксиса метода. Несмотря на краткость, получившиеся макеты могут не подходить в зависимости от вашего теста — методы, настроенные с возвращаемыми значениями, будут возвращать один и тот же экземпляр при каждом вызове. Если это проблема, рассмотрите возможность использования свободного синтаксиса — хотя ваши макеты будут более подробными, они будут гораздо более гибкими.

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

Первоначально опубликовано на https://webdeveloperdiary.substack.com.