В части прошлой недели мы рассмотрели два способа, которыми мы можем настроить тестовые макеты с помощью Moq.

  • Мы можем сделать это быстро и лаконично с помощью нового синтаксиса Linq to Mocks или…
  • Мы можем использовать свободный синтаксис, который предлагает больший контроль.

Мы отметили, что было выгодно использовать Linq to Mocks для более простых макетов и что нам следует выбирать свободный синтаксис там, где необходима точная настройка поведения макета. В этой части давайте рассмотрим еще одну причину использования свободного синтаксиса.

Функциональное программирование против использования состояния

Написание кода с мышлением функционального программирования — отличная практика. Избегая использования состояния, код становится проще:

  • Запускайте параллельно (где необходимо). Поскольку общего состояния нет, один поток не может влиять на результаты другого.
  • Причина с. Чистые функции идемпотентны: вывод всегда будет одним и тем же для заданного набора входных данных.
  • Пишите тесты для. У вас есть результат, против которого можно утверждать.

Все они важны с точки зрения тестируемости. Но бывают случаи, когда функциональное написание невозможно/невозможно. Когда нам нужно написать void методы, у нас должны быть тесты и для них.

Представьте, что вы пишете приложение, включающее службу данных. Это (в настоящее время) имеет один метод: SaveData. Наш код пока выглядит следующим образом:

public interface ILogger
{
    public void Error(string message);
}

public interface IDataRepository
{
    public void Save(string data);
}

public class DataService
{
    private readonly IDataRepository _repository;
    private readonly ILogger _logger;

    public DataService(IDataRepository repository, ILogger logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public void SaveData(string data)
    {
        try
        {
            _repository.Save(data);
        }
        catch (Exception)
        {
            _logger.Error("An error occurred");
        }
    }
}

Мы обернули строку _repository.Save(data) оператором try/catch — мы хотим зарегистрировать ошибку, если возникнет проблема при фиксации данных. При модульном тестировании этого поведения мы хотим настроить макеты для каждой из двух зависимостей службы данных. Нам нужно:

  1. IDataRepository, который вызовет исключение при вызове метода SaveData.
  2. ILogger, который будет записывать аргументы, предоставленные для метода Error.

Чтобы помочь с (1), давайте создадим собственное исключение для использования в наших тестах:

public class TestingException : Exception
{
}

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

Итак, как мы их настроим?

Настройка методов пустоты

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

  • Throws, из-за чего макет выдает исключение указанного типа.
  • Callback. Это запускает логику без возврата данных.

С помощью этих двух методов мы можем написать следующий тест:

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

    var repository = new Mock<IDataRepository>();
    var logger = new Mock<ILogger>();
    var logs = new List<string>();

    repository
        .Setup(r => r.Save(It.IsAny<string>()))
        .Throws<TestingException>();
    
    logger
        .Setup(l => l.Error(It.IsAny<string>()))
        .Callback<string>(msg => logs.Add(msg));
    
    var service = new DataService(repository.Object, logger.Object);

    // Act

    service.SaveData("Save");

    // Assert

    Assert.That(logs.Count, Is.EqualTo(1));
    Assert.That(logs[0], Is.EqualTo("An error occurred"));
}

Здесь мы использовали Throws, чтобы настроить наш IDataRepository для выдачи пользовательского TestingException при вызове Save с любым аргументом. Мы также настроили наш регистратор с Callback, чтобы он делал что-то, когда вызывается Error: он добавляет предоставленную строку в список logs, чтобы мы могли проверить ее на этапе утверждения теста.

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

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

Ранее мы рассмотрели, как настроить макеты с Moq для возврата значений на основе их входных аргументов: написание кода с мышлением функционального программирования может быть полезным и упрощает написание тестов. Однако мы часто пишем функциональные и void методы на C#. При тестировании макеты можно настроить так, чтобы они генерировали исключения и выполняли обратные вызовы в дополнение к возвращаемым значениям.

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

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