В этой статье мы рассмотрим лучшие практики модульного тестирования. Сначала я объясню, что такое модульное тестирование и почему мы должны использовать его в наших проектах. Затем мы рассмотрим лучшие практики модульного тестирования. Я приведу пример кода с использованием фреймворка xUnit для написания модульных тестов в проектах на .Net.

Содержание
Что такое модульное тестирование?
Почему мы должны использовать юнит-тесты в наших проектах?
Лучшие практики
1. Наименование
2. AAA паттерн
3. Старайтесь не применять сложную логику
4. Избегайте нескольких действий
5. Используйте вспомогательные методы для настроек
6. Тестируйте только один компонент
7. Изолированные тесты – использование заглушек
8. Сфокусируйтесь на наиболее эффективных методах
9. Unit-тесты должны быть быстрыми
10. Не используйте “магические строки”
Вывод
Что такое модульное тестирование?
Модульные, или юнит-тесты используются для изолированного тестирования наименьших функциональных модулей программы (методов, функций, классов). Такие тесты проверяют модули на соответствие требованиям или насколько корректно они выполняют свои функции.
Юнит-тесты обычно пишутся разработчиками и находятся на самом базовом уровне жизненного цикла тестирования.

Почему мы должны использовать юнит-тесты в наших проектах?
Юнит-тесты могут принести нашим проектам множество преимуществ:
Качество кода: с помощью юнит-тестов мы можем обнаруживать возможные ошибки на ранних этапах и оперативно их устранять. Таким образом ПО получается качественным и работает максимально эффективно
Хорошая документация: юнит-тесты – это очень полезный инструмент для документирования. Это упрощает другим людям понимание кода. Тесты наглядно показывают использование кода и ожидаемые результаты
Раннее обнаружение багов: мы можем обнаружить потенциальные ошибки с помощью юнит-тестов сразу в момент разработки. Таким образом, мы исправляем баги до внедрения в продовское окружение и не оказываем негативного влияния на код в продакшне
Рефакторинг кода: при рефакторинге кода с помощью юнит-тестов, мы можем проверить, работают ли алгоритмы так, как должны
Экономия затрат: юнит-тесты снижают затраты на разработку, поскольку позволяют исправить ошибки кода до его развертывания
Agile Process: это основное преимущество модульного тестирования. Когда я изменяю алгоритм или добавляю новую функцию, юнит-тесты выступают в качестве подстраховки, гарантируя, что существующий функционал не пострадает от этих изменений
Возможно, вы скажете, что разработка юнит-тестов отнимает много времени. Тем не менее, юнит-тесты являются эффективным средством для выявления и устранения ошибок в текущем и будущем коде.
Лучшие практики
Хороший юнит-тест должен быть читаемым, изолированным, надежным, простым, быстрым и актуальным.
1. Наименование
Название юнит-теста должно четко описывать его назначение. Мы должны понимать что делает метод по его названию, не заглядывая в сам код. Это упрощает документирование и читабельность. Также, когда тесты падают, мы можем определить, какие сценарии выполняются некорректно.
Название модульного теста должно содержать три элемента. Имя тестируемого метода, сценарий тестирования и ожидаемое поведение.
MethodName_StateUnderTest_ExpectedBehavior
2. AAA паттерн
Код юнит-теста должен быть легко читаемым и понятным. Для этого, при разработке, мы используем паттерн AAA. Паттерн AAA очень важный и распространенный паттерн среди юнит-тестов. Он обеспечивает четкое разделение между настройкой тестовых объектов, действиями и результатами. Он разделяет методы юнит-тестов на три части. Arrange, Act и Assert.
В Arrange мы создаем и настраиваем необходимые для тестирования объекты
В Act мы вызываем тестируемый метод и получаем фактический результат
В Assert мы сравниваем ожидаемый и фактический результат. Ассерты определяют, провален или пройден тест. Если ожидаемый и фактический результат совпадает, тест пройден
[Fact]
public void IsPrime_WhenNumberIsPrime_ReturnsTrue()
{
// Arrange
var primeUtils = new PrimeUtils();
int number = 5;
bool expected = true;
// Act
var actual = primeUtils.IsPrime(number);
// Assert
Assert.Equal(expected, actual);
}
3. Старайтесь не применять сложную логику
Избегайте логических условий, таких как if, for, while, switch. Не следует создавать какие-либо данные в пределах метода тестирования. Ориентируйтесь только на результат.
Плохой пример :
[Fact]
public void IsPrime_WhenNumberIsPrime_ReturnsTrue()
{
// Arrange
var primeUtils = new PrimeUtils();
var testCases = new []{2, 3, 5};
bool expected = true;
// Act and Assert
foreach (var number in testCases)
{
// Act
var result = primeUtils.IsPrime(number);
// Assert
Assert.Equal(expected, result);
}
}
Хороший пример :
[Theory]
[InlineData(2,true)]
[InlineData(3,true)]
[InlineData(5,true)]
public void IsPrime_WhenNumberIsPrime_ReturnsTrue(int number, bool expected)
{
// Arrange
var primeUtils = new PrimeUtils();
// Act
var actual = primeUtils.IsPrime(number);
// Assert
Assert.Equal(expected, actual);
}
Это более чистый, читабельный, менее сложный и быстрый вариант.
4. Избегайте нескольких действий
Для каждого действия необходимо создать отдельный тестовый метод. Это позволит четко определять, какой сценарий завершился с ошибкой.
Плохой пример :
[Fact]
public void AdditionAndSubtraction_IntegerNumbers_ReturnsSumAndDifference()
{
// Arrange
var calculator = new Calculator();
int number1 = 9;
int number2 = 5;
int expected1 = 14;
int expected2 = 4;
// Act
var actual1 = calculator.Add(number1, number2);
var actual2 = calculator.Subtract(number1, number2);
// Assert
Assert.Equal(expected1, actual1);
Assert.Equal(expected2, actual2);
}
Хороший пример :
[Theory]
[InlineData(2, 3, 5)]
[InlineData(11, 5, 16)]
public void Add_TwoNumbers_ReturnsSum(int number1, int number2, int expected)
{
// Arrange
var calculator = new Calculator();
// Act
var actual = calculator.Add(number1, number2);
// Assert
Assert.Equal(expected, actual);
}
[Theory]
[InlineData(2, 3, -1)]
[InlineData(11, 5, 6)]
public void Subtract_TwoNumbers_ReturnsDifference(int number1, int number2, int expected)
{
// Arrange
var calculator = new Calculator();
// Act
var actual = calculator.Subtract(number1, number2);
// Assert
Assert.Equal(expected, actual);
}
5. Используйте вспомогательные методы для настроек
Если нам нужны одинаковые объекты для нескольких тестовых методов, лучше создавать вспомогательные методы. Это позволяет избежать дублирующегося кода. Кроме того, это повышает читаемость и поддерживаемость тестов. Такой подход уменьшает повторения кода и обеспечивает более эффективное управление изменениями.
Плохой пример :
[Theory]
[InlineData(2, 3, 5)]
[InlineData(11, 5, 16)]
public void Add_TwoNumbers_ReturnsSum(int number1, int number2, int expected)
{
// Arrange
var calculator = new Calculator();
// Act
var actual = calculator.Add(number1, number2);
// Assert
Assert.Equal(expected, actual);
}
Хороший пример :
[Theory]
[InlineData(2, 3, 5)]
[InlineData(11, 5, 16)]
public void Add_TwoNumbers_ReturnsSum(int number1, int number2, int expected)
{
// Arrange
var calculator = CreateCalculator();
// Act
var actual = calculator.Add(number1, number2);
// Assert
Assert.Equal(expected, actual);
}
private Calculator CreateCalculator()
{
return new Calculator();
}
6. Тестируйте только один компонент
В одном юнит-тесте должен проверяться только один модуль. Это может быть, возвращаемое значение, изменение состояния системы или обращение к стороннему объекту. К примеру, если ваш юнит-тест содержит проверки более одного объекта, это может означать, что он тестирует сразу несколько вещей.
7. Изолированные тесты – использование заглушек
Методы юнит-тестов должны быть независимыми. Но некоторые методы могут иметь зависимости от внешних сервисов, таких как базы данных или веб-сервисы. Для имитации таких зависимостей мы можем создавать объекты-заглушки (mock-objects) с помощью библиотеки moq. С помощью таких заглушек, мы можем изолировать тестируемый код и сосредоточиться только на поведении тестируемого блока. Кроме того, изолированные юнит-тесты выполняются быстрее.
Для изолирования мы используем библиотеку Moq. Установите ее из менеджера пакетов NuGet.

public class BooksControllerTest
{
[Fact]
public void GetAll_ReturnsOkResultWithBooks()
{
// Arrange
var mockService = new Mock<IBookService>();
mockService.Setup(n => n.GetAll()).Returns(MockData.GetTestBookItems());
var booksController = new BooksController(mockService.Object);
// Act
var result = booksController.GetAll();
// Assert
Assert.IsType<OkObjectResult>(result);
var bookList = result as OkObjectResult;
Assert.IsType<List<Book>>(bookList.Value);
}
}
8. Сфокусируйтесь на наиболее эффективных методах
Вместо простых, понятных методов следует тестировать те методы, которые влияют на поведение системы, поскольку они содержат сложную логику.
9. Unit-тесты должны быть быстрыми ?
Очень важно, чтобы модульные тесты были быстрыми. Это ускоряет процесс разработки и дает возможность их часто запускать. Быстрое тестирование позволяет обнаруживать и исправлять ошибки на более ранних этапах.
Как сделать наши тесты максимально быстрыми?
Сделайте их простыми
Используйте объекты-заглушки (mock-objects) для внешних зависимостей
Сделайте их независимыми от других тестов
10. Не используйте “магические строки”
Захардкоженные магические строки и числа (когда невозможно понять, что означает тот или иной объект по его названию), создают проблемы при модульном тестировании. Может быть непонятно, для чего нужен тот или иной объект, что может привести к ошибкам при тестировании и поддержке. Вместо использования напрямую таких “магических” обозначений следует применять константы с осмысленными, понятными именами. Значения констант находятся в одном месте, а понятные названия улучшают читаемость кода.
Плохой пример :
[Fact]
public void addIdentityNumberTr_WhenNumberIsNotElevenDigit_ThrowsException()
{
// Arrange
var identityManager = new IdentityManager();
// Act
Action actual = () => identityManager.addIdentityNumberTr("123456789");
// Assert
Assert.Throws<Exception>(actual);
}
Хороший пример :
[Fact]
public void addIdentityNumberTr_WhenNumberIsNotElevenDigit_ThrowsException()
{
// Arrange
var identityManager = new IdentityManager();
const string INVALID_IDENTITY_NUMBER = "123456789";
// Act
Action actual = () => identityManager.addIdentityNumberTr(INVALID_IDENTITY_NUMBER);
// Assert
Assert.Throws<Exception>(actual);
}
Вывод
Лучшие практики модульного тестирования очень важны для обеспечения надежности и поддерживаемости кода. Понятная документация и краткие тестовые методы облегчают понимание их работы и упрощают адаптацию к изменениям. Следование этим практикам позволяет разработчикам тестов создавать надежный и проверенный код.