Pull to refresh

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

Reading time7 min
Views4.1K

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

}

Вывод

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

Tags:
Hubs:
Total votes 4: ↑3 and ↓1+3
Comments1

Articles