В части прошлой недели мы рассмотрели два способа, которыми мы можем настроить тестовые макеты с помощью 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
— мы хотим зарегистрировать ошибку, если возникнет проблема при фиксации данных. При модульном тестировании этого поведения мы хотим настроить макеты для каждой из двух зависимостей службы данных. Нам нужно:
IDataRepository
, который вызовет исключение при вызове методаSaveData
.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.