Макет метода з out параметром за допомогою Moq

Код проекту

Вступ

Moq – зручна та популярна бібліотека створення макетів об’єктів для платформи .Net. Вона використовується при модульному тестуванні, щоб ізолювати окремі класи від їх залежностей та впевнитись, що виконуються лише очікувані методи залежних об’єктів.

Коли створюються макети об’єктів, їх методи, які мають бути виконані, встановлюються за допомогою різноманітних перевизначень методу Setup. Проте коректно побудувати макет об’єкта може бути досить складною задачею, наприклад, коли метод має ref/out параметри чи є статичним. Нижче розглянуто випадок побудови макета метода із out параметром.

В статті використовуються функціональності, які описано в документі Moq’s Quickstart.

Оригінал статті опубліковано на Codeguida.

Перелік технологій

Наведена програма використовує C#7, .Net 4.6.1, та NuGet пакети Moq, FluentAssertions та xUnit.

Постановка задачі

Розглянемо інтерфейс сервісу, який виставляє метод із out параметром:

public interface IService
{
	void ProcessValue(string inputValue, out string outputValue);
}

та простий клас, що використовує цей метод:

public class Class1
{
	private readonly IService _service;

	public Class1(IService service)
	{
		_service = service;
	}

	/// <summary>
	/// Повертає усічене значення вхідного рядка, обробленого сервісом.
	/// </summary>
	/// <param name="inputValue">Вхідне значення, може бути null.</param>
	/// <param name="outputValue">Вихідне значення.</param>
	public string ProcessValue(string inputValue)
	{
		_service.ProcessValue(inputValue, out string outputValue);
		return outputValue;
	}
}

Потрібно написати модульні тести для методу ProcessValue класу Class1. Очевидним чином для цього необхідно створити макет об’єкту сервісу Service1.

Макет сервісу без метода зворотного виклику

Як зазначено в документації Moq’s Quickstart, макет методу із out параметром може бути задано наступним кодом:

// вихідні аргументи
var outString = "ack";
// TryParse поверне true, а вихідний аргумент буде мати значення “ack”, що присвоюєно відкладеним чином
mock.Setup(foo => foo.TryParse("ping", out outString)).Returns(true);

Цей підхід використаємо в першому тесті:

[Theory]
[InlineData(null)]
[InlineData("short")]
[InlineData("\t\t\ttabbed\t\t")]
[InlineData("long long value")]
public void Class1_Return_ConstValueWithoutCallback(string inputValue)
{
	// налаштування
	var expectedValue = "Expected value";
	var service = new Mock<IService>();
	service
		.Setup(mock => mock.ProcessValue(It.IsAny<string>(), out expectedValue))
		.Verifiable();
 
	// дія
	var class1 = new Class1(service.Object);
	var actualValue = class1.ProcessValue(inputValue);

	// підтверження
	actualValue.Should().NotBeNull();
	actualValue.Should().Be(expectedValue);

	service.Verify();
}

В цьому випадку out параметр має попередньо задане значення, яке не залежить від решти змінних, що використовує тест. Більше того, якщо твердження теста припускає, що значення out параметру залежить від вхідних параметрів тесту, то попередній тест можна змінити таким чином:

[Theory]
[InlineData(null)]
[InlineData("short")]
[InlineData("\t\t\ttabbed\t\t")]
[InlineData("long long value")]
public void Class1_Return_TheSameValueWithoutCallback(string inputValue)
{
	// налаштування
	var expectedValue = $"Output {inputValue}";
	var service = new Mock<IService>();
	service
		.Setup(mock => mock.ProcessValue(It.IsAny<string>(), out expectedValue))
		.Verifiable();

	// той же код
	// ...
}

Даний підхід може бути корисним, проте, якщо необхідно отримати значення решти параметрів, які передано до методу сервісу на кшталт inputValue, чи встановити певне значення out параметру, то потрібно при створенні макету використовувати метод Callback.

Макет сервісу із методом зворотного виклику

Відповідно до Moq’s Quickstart, можна побудувати макет методу із ref / out параметрами з методом зворотного виклику:

// методи зворотного виклику можливі для методів з `ref` / `out` параметрами, але вони вимагають додаткової роботи (та Moq 4.8+):
delegate void SubmitCallback(ref Bar bar);
 
mock.Setup(foo => foo.Submit(ref It.Ref<Bar>.IsAny))
    .Callback(new SubmitCallback((ref Bar bar) => Console.WriteLine("Submitting a Bar!")));

Тому потрібно визначити делегат з точно таким же переліком параметрів, що і метод ProcessValue із сервісу IService:

private delegate void ServiceProcessValue(string inputValue, out string outputValue);

Тепер макет сервісу IService можна визначити за допомогою методу Callback:

[Theory]
[InlineData(null)]
[InlineData("short")]
[InlineData("\t\t\ttabbed\t\t")]
[InlineData("long long value")]
public void Class1_Return_NewValueWithCallback(string inputValue)
{
	// налаштування
	string actualInputValue = null;

	const string outputValue = "Inner value";
	var expectedValue = "Not used value";
	var service = new Mock<IService>();
	service
		.Setup(mock => mock.ProcessValue(It.IsAny<string>(), out expectedValue))
		.Callback(new ServiceProcessValue(
			(string input, out string output) =>
			{
				actualInputValue = input;
				output = outputValue;
			}))
		.Verifiable();

	// дія
	var class1 = new Class1(service.Object);
	var actualValue = class1.ProcessValue(inputValue);

	// підтверження
	actualValue.Should().NotBeNull();
	actualValue.Should().Be(outputValue);

	actualInputValue.Should().Be(inputValue);

	service.Verify();
}

Метод Callback використовує делегат для того, щоб зберегти значення outputValue в out параметрі, як показано в 20-тому рядку, та запам’ятати значення переданих параметрів, як показано в 19-тому рядку. Зазначимо, що при цьому значення, яке було використано в визначенні метода Setup, не використовується. Цей факт безпосередньо перевіряється на 32-тому рядку.

Код проекту

Твердження статті демонструються в проекті, що складається із консольного застосунку ConsoleApp та бібліотеки тестів ConsoleApp.Tests.

Консольний застосунок ConsoleApp містить інтерфейс IService та клас Service, як реалізацію інтерфейсу, та клас Class1, що використовує сервіс. В головному методі застосунку створюється об’єкт класу та метод ProcessValue перевіряється на декількох значеннях.

Бібліотека тестів ConsoleApp.Tests містить тести, що розглянуто вище.


1. Усі використані IP-адреси, імена серверів, робочих станцій, доменів, є вигаданими та використовуються виключно в демонстраційних цілях.
2. Інформація викладається на умовах «AS IS».

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.