- Что такое модульный тест
- Зачем утруждать себя написанием модульных тестов
- Обнаруживайте ошибки как можно раньше
- Документация
- Краткое введение в разработку на основе тестирования (TDD)
- Практика написания модульных тестов
- Пишем осмысленное имя теста
- Каждый тест должен охватывать только 1 сценарий
- Используйте шаблон AAA
- Изолируйте свой юнит от внешних зависимостей
- Избегайте тестирования детализированной реализации
- Работа над устаревшим кодом: должен ли я сначала отрефакторить или написать тест?
- Вывод
Что такое модульный тест
Модульный тест – это категория тестов с самой высокой степенью детализации, основанная на тестовой пирамиде. Обычно он ориентирован на функциональность класса, функции или компонента пользовательского интерфейса и изолирован от внешней системы, такой как базы данных и сторонние API.
Зачем утруждать себя написанием модульных тестов
В большинстве случаев задача написания/рефакторинга кода заключается в том, чтобы убедиться, что вы не нарушаете существующую функциональность. Раньше разработчику нужно было проверить измененный класс/функцию вручную, чтобы убедиться, что ничего не сломалось. Ручная работа подвержена ошибкам. Разработчики могут забыть некоторые тестовые случаи, и код с багами отправляется в продакшен. Наличие модульных тестов и их правильная настройка на CI избавит вас от таких сценариев. Следовательно, это повысит вашу уверенность в CD до продакшена.
Обнаруживайте ошибки как можно раньше
Модульные тесты пишутся (и должны быть написаны) изолированно, поэтому их можно выполнять без необходимости разворачивать внешние службы и запускать такие инструменты, как puppeteer. Он может работать быстро и гораздо менее требователен к памяти по сравнению с сквозными тестами. Это уникальное свойство модульных тестов позволяет разработчикам выполнять тесты столько, сколько необходимо в процессе разработки.
Некоторые тестранеры, такие как jest, предоставляют возможность наблюдать за запуском тестов каждый раз, когда в код вносятся изменения, что еще больше облегчает обнаружение ошибок во время разработки.
Документация
Тщательно написанные тесты могут выступать в качестве документации, поскольку они описывают желаемое поведение конкретной части программного обеспечения. Я также нахожу, что тесты очень полезны во время процесса проверки кода. Они дают рекомендации по поведению программного обеспечения и избавляют от необходимости подробно разбираться в деталях реализации, чтобы понять его функциональность.
Краткое введение в разработку на основе тестирования (TDD)
Разговор о модульных тестах не будет полным без упоминания Test-Driven Development (TDD). В двух словах TDD можно охарактеризовать как красно-зеленый-рефакторинг подход к разработке программного обеспечения.
- Вы начинаете с написания одного теста, чтобы охватить одно требование. Тест должен быть провален, так как у вас нет работающей реализации системы (красный).
- Вы пишете реализацию, чтобы она прошла тест (зеленый).
- Отрефакторите свой код (если это необходимо).
- Переходите к следующему требованию и возвращайтесь к шагу 1
Ключевой вывод из TDD заключается в том, чтобы позволить тестам управлять вашей архитектурой, а не наоборот.
Практика написания модульных тестов
Я буду использовать образец, написанный на javascript + jest, так как это язык и тестовый фреймворк, с которыми мне наиболее комфортно. В качестве примера мы используем класс dateFormatter со следующей спецификацией:
- Этот класс имеет публичный метод format, которая принимает объект Javascript Date в качестве входных данных и возвращает строку даты в формате dd-mm-yyyy.
- Если входные данные являются недопустимым объектом даты, он вызовет исключение.
Пишем осмысленное имя теста
// Плохо
test('format should format date correctly', function (){
...
})// Хорошо
test('format should return date with dd-mm-yyyy format given a valid date object input', function (){
...
})
Золотое правило содержательного названия теста – это четкое описание выходных и входных данных. Читатель должен быть в состоянии понять желаемое поведение без необходимости читать детали реализации тестируемой системы.
Каждый тест должен охватывать только 1 сценарий
describe('DateFormatter', function() {
// Плохо
test('format should return the date with following format:dd-mm-yyyy given valid date object and throw exception if the input is invalid date object', function(){
...
}) // Хорошо
test('dateFormatter should return the date with following format:dd-mm-yyyy given valid date object', function() {
...
}) test('dateFormatter should throw exception given invalid date object', function() {
...
})
})
Следует избегать тестирования двух функций в рамках одного теста. Причина этого принципа заключается в том, что если тест проваливается, мы не знаем, какая функция проваливается. Вам нужно будет проверить обе функции, даже если только одна из них не пройдет тест.
Используйте шаблон AAA
Шаблон Arrange, Act, Assert – это распространенный шаблон, который можно использовать для улучшения читабельности теста, разделяя части теста пустой строкой.
- Arrange готовит необходимые приспособления, насмешки, заглушки и тестируемую систему.
- Act выполняет тестируемую функциональность
- Assert-это утверждение результата выполнения относительно желаемого значения
describe('DateFormatter', function() {
test('format should return the date with following format:dd-mm-yyyy given valid date object', function() {
// подготовка данных
const sut = DateFormatter();
const date = new Date('2020-01-01');
// выполнение логики
const result = sut.format(date);
// сверка результатов
expect(result).toBe('01-01-2020')
})
})
Изолируйте свой юнит от внешних зависимостей
Допустим, мы добавили еще одну функциональность поверх класса. Каждый раз, когда метод format выполняется, он будет регистрировать результат в стороннем API с помощью функции logToExernalAPI.
import { logToExternalAPI } from './third-party-services'; class DateFormatter() {
format(date) {
...
logToExternalAPI(result);
return result;
}
}
Как написать тест для этой новой функциональности? Один из подходов заключается в рефакторинге класса и использовании инъекции зависимостей, чтобы избежать прямой зависимости от другого блока. Используя эту технику, мы также улучшаем дизайн класса, отделяя его от реализации logger.
class DateFormatter {
constructor(logger){
this.logger = logger;
}
format(date) {
...
this.logger(result);
return result;
}
}
describe('DateFormatter', function(){
test('format should call logger with the formatted given valid date object', function() {
// Arrange
const loggerMock = jest.fn();
const sut = DateFormatter(loggerMock);
const date = new Date('2020-01-01');
// Act
sut.format(date); // Assert
expect(loggerMock).toBeCalledWith('01-01-2020')
})
})
Избегайте тестирования детализированной реализации
Пример детализации реализации тестирования выглядит следующим образом:
- Проверка последовательности вызовов функций
- Проверка внутреннего состояния класса
Следует избегать деталей реализации тестирования, поскольку это создает тесную связь между тестами и реализацией. Например, если вы пишете тесты для проверки последовательности вызовов функций внутри метода вашего класса и решаете изменить порядок, тест не будет выполнен, даже если он на самом деле не влияет на пользователя вашего класса.
Работа над устаревшим кодом: должен ли я сначала отрефакторить или написать тест?
В некоторых случаях конкретный класс/компонент, с которым вы хотите работать, написан таким образом, что его очень трудно проверить.
В общем, я бы предложил написать тест перед рефакторингом, если это не очень трудно сделать.
Вывод
Написание модульных тестов является важной и широко распространенной практикой повышения качества программного обеспечения, предоставляя средства для обеспечения правильности программного обеспечения и позволяя разработчику обнаруживать ошибки как можно раньше.
В конце концов, эта техника (как и другие техники) потребует некоторой практики и дисциплины для овладения. Я надеюсь, что эта статья может дать вам некоторое базовое представление о том, как и почему писать хорошие тесты для вашего программного обеспечения.
Счастливого кодинга!