Лучшие практики модульного тестирования

Перевод статьи «Unit Testing Best Practices».

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

Смотрите также: “Лучшие практики юнит-тестирования на Java”

юнит-тестирование

Содержание:

БЕСПЛАТНО СКАЧАТЬ КНИГИ в телеграм канале "Библиотека тестировщика"

Что такое модульное тестирование?

Модульные, или юнит-тесты используются для изолированного тестирования наименьших функциональных модулей программы (методов, функций, классов). Такие тесты проверяют модули на соответствие требованиям или насколько корректно они выполняют свои функции.

Юнит-тесты обычно пишутся разработчиками и находятся на самом базовом уровне жизненного цикла тестирования.

пирамида тестирования

Почему мы должны использовать юнит-тесты в наших проектах?

Юнит-тесты могут принести нашим проектам множество преимуществ:

  • Качество кода: с помощью юнит-тестов мы можем обнаруживать возможные ошибки на ранних этапах и оперативно их устранять. Таким образом ПО получается качественным и работает максимально эффективно
  • Хорошая документация: юнит-тесты – это очень полезный инструмент для документирования. Это упрощает другим людям понимание кода. Тесты наглядно показывают использование кода и ожидаемые результаты
  • Раннее обнаружение багов: мы можем обнаружить потенциальные ошибки с помощью юнит-тестов сразу в момент разработки. Таким образом, мы исправляем баги до внедрения в продовское окружение и не оказываем негативного влияния на код в продакшне
  • Рефакторинг кода: при рефакторинге кода с помощью юнит-тестов, мы можем проверить, работают ли алгоритмы так, как должны
  • Экономия затрат: юнит-тесты снижают затраты на разработку, поскольку позволяют исправить ошибки кода до его развертывания
  • Agile Process:  это основное преимущество модульного тестирования. Когда я изменяю алгоритм или добавляю новую функцию, юнит-тесты выступают в качестве подстраховки, гарантируя, что существующий функционал не пострадает от этих изменений

Возможно, вы скажете, что разработка юнит-тестов отнимает много времени. Тем не менее, юнит-тесты являются эффективным средством для выявления и устранения ошибок в текущем и будущем коде.

Лучшие практики

Хороший юнит-тест должен быть читаемым, изолированным, надежным, простым, быстрым и актуальным.

1. Наименование

Название юнит-теста должно четко описывать его назначение. Мы должны понимать что делает метод по его названию, не заглядывая в сам код. Это упрощает документирование и читабельность. Также, когда тесты падают, мы можем определить, какие сценарии выполняются некорректно.

Название модульного теста должно содержать три элемента. Имя тестируемого метода, сценарий тестирования и ожидаемое поведение.

MethodName_StateUnderTest_ExpectedBehavior

[Fact]

public void IsPrime_WhenNumberIsPrime_ReturnsTrue()
{
  var primeUtils = new PrimeUtils(); 
  int number = 5;
  bool expected = true;
  
  var actual = primeUtils.IsPrime(number);

  Assert.Equal(expected, actual);
}

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
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);
}

Вывод

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

тест пройден

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *