Макет метода з 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>
	/// Вхідне значення, може бути null.
	/// Вихідне значення.
	public string ProcessValue(string inputValue)
	{
		_service.ProcessValue(inputValue, out string outputValue);
		return outputValue;
	}
}

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

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

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

// out arguments
var outString = "ack";
// TryParse will return true, and the out argument will return "ack", lazy evaluated
mock.Setup(foo => foo.TryParse("ping", out outString)).Returns(true);

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

[Theory]
[InlineData("input")]
public void Class1_Return_ConstValueWithoutCallback(string inputValue)
{
	var expectedValue = "Expected value";
	var service = new Mock();
	service
		.Setup(mock => mock.ProcessValue(It.IsAny(), out expectedValue))
		.Verifiable();

	// tested routine
	var class1 = new Class1(service.Object);
	var actualValue = class1.ProcessValue(inputValue);

	// assertion
	actualValue.Should().NotBeNull();
	actualValue.Should().Be(expectedValue);

	service.Verify();
}

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

[Theory]
[InlineData("input")]
public void Class1_Return_TheSameValueWithoutCallback(string inputValue)
{
	var expectedValue = $"Output {inputValue}";
	var service = new Mock();
	service
		.Setup(mock => mock.ProcessValue(It.IsAny(), out expectedValue))
		.Verifiable();
	// the same code
	// ...
}

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

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

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

// callbacks for methods with `ref` / `out` parameters are possible but require some work (and Moq 4.8 or later):
delegate void SubmitCallback(ref Bar bar);

mock.Setup(foo => foo.Submit(ref It.Ref.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("input")]
public void Class1_Return_NewValueWithCallback(string inputValue)
{
	string actualInputValue = null;

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

	// tested routine
	var class1 = new Class1(service.Object);
	var actualValue = class1.ProcessValue(inputValue);

	// assertion
	actualValue.Should().NotBeNull();
	actualValue.Should().Be(outputValue);

	actualInputValue.Should().Be(inputValue);

	service.Verify();
}

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

Код проекту

Твердження статті демонструються в проекті, що складається із консольного застосунку 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.