В этой статье мы рассмотрим лучшие практики модульного тестирования. Сначала я объясню, что такое модульное тестирование и почему мы должны использовать его в наших проектах. Затем мы рассмотрим лучшие практики модульного тестирования. Я приведу пример кода с использованием фреймворка 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.

библиотека moq
библиотека moq
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);

}

Вывод

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