Тестирование кода — один из важнейших аспектов разработки программного обеспечения. Оно обеспечивает качество, масштабируемость и надежность продукта.
Но самостоятельно, без всяких рекомендаций, может быть трудно написать эффективные тесты. Более того, код для тестирования может стать сложнее и труднее в сопровождении, чем собственно рабочий код!
Не позволяйте этому случиться с вами. Следуйте лучшим практикам юнит-тестирования, и вы сможете писать компактный, точный и легко читаемый код тестов. Это позволит вам сэкономить драгоценное время, избежать технического долга и получить удовольствие от тестирования кода.
1. Юнит-тестирование должно быть бережливым и точным
Код тестов должен быть простым и удобным для работы. Любой, кто смотрит на тест, должен сразу понимать, о чем идет речь. Разработка тестов должна приносить большую пользу при очень малых затратах сил и времени.
Вам требуется более 30 секунд для прочтения и понимания теста? Перепишите его!
Тестируете только ради процента покрытия? Не надо!!! Это делает большую часть тестового набора ненужной и нежелательной для разработчиков. Тестируйте только то, что необходимо. Лучше отказаться от некоторых тестов для повышения гибкости и простоты, проверяя только основную бизнес-логику и основные граничные случаи.
2. Тестируйте поведение, а не реализацию
Не стоит проверять каждую строчку и изменение внутренних переменных в коде. При тестировании следует сосредоточиться на результате. Результат должен оставаться неизменным, даже если код внутри метода подвергся рефакторингу!
Благодаря такому подходу вам не придется переписывать тесты при изменении кодовой базы.
// Неправильно ❌ - Тестирование реализации describe('Evaluation Service', () => { describe('Register Students', () => { it('Should add new students to the evaluation service', () => { const studentJosh = { id: 1, name: 'Josh McLovin', average: 6.98, } evaluationService.addStudent(studentJosh) expect(evaluationService._students[0].name).toBe('Josh') expect(evaluationService._students[0].average).toBe(6.98) }) }) })
// Правильно ✅ - Тестирование поведения describe('Evaluation Service', () => { describe('Register Students', () => { it('Should add new students to the evaluation service', () => { const studentJosh = { id: 1, name: 'Josh McLovin', average: 6.98, } evaluationService.addStudent(studentJosh) expect(evaluationService.getStudentAverage('Josh')).toBe(6.98) }) }) })
3. Именование и структурирование тестов
Приходилось ли вам сталкиваться с неудачным тестом, названным » […] должно […] правильно», и терять пару минут на поиск причины проблемы?
Именование и структурирование набора тестов может повысить вашу способность быстро и точно устранять неисправные тесты. Это в конечном итоге сэкономит ваше драгоценное время.
Итак, давайте рассмотрим два ключевых принципа, о которых следует помнить при тестировании.
3.1 Продуманное именование тестов
Подбирая названия для тестов, старайтесь включать в них следующую информацию:
- Что тестируется?
- При каких условиях?
- Каков ожидаемый результат?
// Правильно ✅ - Нейминг тестов // 1. What is being tested: describe('Evaluation Service', () => { describe('Evaluate Students', () => { // 2 & 3. Context and expected result it('If the student grade is below the minimum grade, student should be suspended', () => { const students = [ { name: 'Mark', grade: 4.25 }, { name: 'Colin', grade: 6.7 }, { name: 'Ben', grade: 5.3 }, ] const result = evaluationService.evaluateStudents({ students, minGrade: 5 }) expect(result['Mark']).toBe('suspended') }) }) })
3.2 Шаблон AAA для структурирования кода тестов
Если вы хотите поддерживать читаемый и легкий для понимания тестовый набор, структурируйте тест следующим образом:
- Arrange (подготовка). Подготовьте весь код, необходимый для моделирования требуемой ситуации. Это может включать инициализацию переменных, имитацию ответов, инстанцирование тестируемого модуля и т.д.
- Act (действие). Запустите то, что тестируется, обычно в виде одной строки кода.
- Assert (првоерка). Проверьте, соответствует ли полученный результат ожидаемому. Как и в предыдущем случае, это должно занимать всего одну строку.
// Правильно - Шаблон тестирования AAA describe('Evaluation Service', () => { describe('Average Calculation', () => { it('Should calculate the average grade of all the students', () => { // Arrange: create an object with the student names and their grades const students = [ { name: 'Mark', grade: 4 }, { name: 'Colin', grade: 10 }, { name: 'Ben', grade: 7 }, { name: 'Tim', grade: 3 }, ] // Act: execute the getAverage method const avg = evaluationService.getAverage(students) // Assert: check if the result is the expected one -> (4+10+7+3)/4 = 6 expect(avg).toEqual(6) }) }) })
4. Тесты должны быть детерминированными и изолированными
Если из-за одного неудачного теста весь набор становится красным, то, возможно, вы не совсем правильно подходите к решению этой проблемы!
Тесты должны быть независимыми и изолированными. У каждого отдельного теста должна быть своя цель. К тому же, он должен работать с одной конкретной логикой за раз. Это позволяет получить более быстрый и стабильный набор тестов.
Что произойдет, если вы не будете писать тесты независимо друг от друга? В таком случае вы не сможете точно определить причину и местоположение ошибок и проблем. При рефакторинге придется обновлять и синхронизировать несколько тестов. Кроме того, вы не сможете запускать тесты в произвольном порядке, а это может привести к нарушению или пропуску некоторых утверждений или ожиданий.
5. Юнит-тестирование на основе свойств и реалистичных данных
Надоело писать в тестах большое количество возможных входных данных? Тестирование на основе свойств сделает это за вас! Но… Что это такое?
Тестирование на основе свойств создает сотни возможных комбинаций, нагружая тест и увеличивая вероятность обнаружения ранее незамеченных ошибок. Такой подход может даже вернуть входные данные, которые могли привести к неожиданному результату.
Такие библиотеки, как JSVerify или Fast-Check, предлагают важные инструменты для облегчения тестирования на основе свойств.
Однако если вы предпочитаете не заниматься столь масштабным тестированием, важно по возможности использовать реалистичные данные. Ввод таких данных, как ‘abc’ или ‘1234’, может ошибочно привести к прохождению теста, когда на самом деле он должен быть провален.
// Неправильно ❌ - Ложно позитивный тест - Тест, который код проходит успешно, хотя и не должен class EvaluationService { _students = []; addStudent(student) { // Добавить студента, если в имени нет цифр if(!student.name.matches(/^([^0-9]*)$/)){ this._students.push(student); } } } describe('Evaluation Service', () => { describe('Register Students', () => { it('Should add students to the Evaluation service', () => { const mockStudent = { id: 2, name: 'username', average: 7 } // Не провалится, потому что имя - строка без цифр -> Мы не проверяем, что случится, если юзер // вводит имя с цифрой. evaluationService.addStudent(mockStudent) expect(evaluationService.getStudentAverage('username')).toBe(7) }) }) }) // В этом примере мы сделали тест таким, чтобы код его прошел, вместо того чтобы поискать граничные значения с реалистичными данными
Бонусный совет!
Если вы испытываете трудности с тестированием какой-либо логики в вашем компоненте, это может свидетельствовать о том, что вам следует разбить логику компонента на более мелкие, простые и тестируемые куски кода.
Следование этим рекомендациям может сделать юнит-тестирование производственного кода более удобным, а тесты — более читаемыми. Спасибо за прочтение!
Проверенное приложение — надежное приложение!
Перевод статьи «The 5 principles of Unit Testing».
[customscript]techrocks_custom_after_post_html[/customscript]
[customscript]techrocks_custom_script[/customscript]