Мне потребовалось несколько лет попыток и неудач, чтобы понять, что именно работает при написании тестов. Вот 6 правил, которым я всегда следую, независимо от того, какие тесты я пишу.
Следуйте шаблону AAA
Чистые модульные тесты следуют шаблону AAA:
- Упорядочить: Настройте тестовый сценарий
- Действовать: Выполнить бизнес-логику
- Утверждать: Выразить некоторые ожидания
Это святой грааль простых тестов.
Одна концепция в одном тесте
Тестируйте одну концепцию в каждом модульном тесте. Если вы следуете шаблону AAA, это должно быть естественным.
Часто я вижу тесты, которые охватывают множество различных концепций в одном модульном тесте. Это запах кода. Лучше разделить такой тест на несколько тестов, каждый из которых проверяет что-то свое.
Эмпирическое правило — если после вашего
assert
стоитact
, вам следует подумать о том, чтобы разделить его на два отдельных теста.
Ваши тесты будет легче понять и легче исправить.
// Bad - mixing two concepts
it('should add new item into cart', () => {
let cart = new Cart()
cart.add(new Item('Shoes')) // <-- // Testing adding item into cart
expect(cart.size).toBe(1);
cart.removeAllItems(); // <-- Testing removing all items from cart
expect(cart.size).toBe(0);
});
// Better - split it up
it('should add new item into cart', () => {
let cart = new Cart();
cart.add(new Item('Shoes');
expect(cart.size).toBe(1);
});
it('should be possible to remove all items from cart', () => {
let cart = new Cart([new Item()]);
cart.removeAllItems();
expect(cart.size).toBe(0);
});
Избегайте логики в ваших тестах
Избегайте любых if
, else
, switch
или троичных операторов в коде ваших тестов.
Как только вы используете любой из операторов потока управления — вы добавили логику в свой тест, и теперь вам нужен тест для вашего теста.
Это часто случается с параметризованными тестами, где вы ожидаете немного разного поведения для разных входов, а также с тестами, где кто-то хотел избежать провала для неработающего теста.
В таких случаях всегда лучше разделить тест на части и сделать его плоским и простым.
// Bad
it("should have focus when clicked", () => {
render(<Input />)
const input = screen.getByRole("input")
// this null check is pointless
if (input !== null) {
click(input)
expect(input).toHaveFocus()
}
})
// Better
it("should have focus when clicked", () => {
render(<Input />)
click(screen.getByRole("input"))
expect(screen.getByRole("input")).toHaveFocus()
})
Оптимизируйте для простоты
Хороший тест — короткий, плоский, простой и приятный в работе. Когда вы смотрите на тест, вы должны сразу понять его замысел.
Не вставляйте тесты друг в друга. Это добавляет ненужную сложность. Каждый уровень вложенности добавляет в код дополнительную когнитивную сложность.
Рефакторите общий код настройки в фикстуру, чтобы избежать повторений.
Только публичный API
Если ваш тест делает что-то, чего не может сделать пользователь, скорее всего, он тестирует детали реализации.
Это запах кода. Любое изменение в базовой реализации приведет к поломке вашего теста.
Тестируйте только API, который может использовать ваш пользователь.
Используйте TDD для обеспечения покрытия
В 10/10 случаев, когда я использую TDD, покрытие кода поднимается выше 80%.
Начните со счастливого пути — убедитесь, что код работает для ваших основных сценариев использования.
Продолжайте с несчастливого пути — проверьте условия ошибки, неожиданный ввод или любые другие побочные случаи.
Вы можете пропустить тестирование тривиальных однострочных строк, геттеров и сеттеров.
Таким образом, вы легко достигнете отметки 80% покрытия.
Не зацикливайтесь на покрытии кода — оно говорит только о том, какие строки были выполнены, а не о том, работают ли они так, как задумано. Сосредоточьтесь больше на написании содержательных тестов.
Вот и все
Соблюдая эти несколько правил, вы сможете писать тесты, которые будут понятны всем, и работать с ними будет одно удовольствие.
Дайте мне знать, есть ли у вас другие правила, которым вы следуете, и почему.