Сегодня я собираюсь обсудить модульные тесты и способы их улучшения. Мы рассмотрим несколько примеров улучшения модульных тестов и удаления дубликатов в коде. Для тестирования я буду использовать jest, а функция для тестирования будет написана на javascript без каких-либо дополнительных фреймворков и библиотек.
В реальных проектах у вас определенно будет важная функция, которую нелегко изменить. Но менять, конечно, придется. Было бы здорово, если бы перед его изменением у вас уже был для него юнит-тест. Но сейчас для нас это не проблема. Я предоставлю потенциальные начальные тесты, и мы постараемся их улучшить.
Функция тестирования
Давайте представим, что у нас есть функция, которая принимает строку в качестве параметра, проверяет эту строку и возвращает логическое значение. Это может быть функция, которая проверяет имена и принимает только латинские буквы, а может выглядеть так.
const validateName = (name) => { // Some code const isLatin = ...; if (isLatin) { return true; } return false; };
Начальные модульные тесты
В основном разработчики стараются прокладывать только счастливые пути. Часто основной целью тестирования является охват. Такой тест не поможет разработчикам предотвратить ошибки при рефакторинге.
import { validateName } from './validator.js'; it('validateName() - should return valid value', () => { expect(validateName('l')).toBe(true); expect(validateName('a')).toBe(true); expect(validateName('c')).toBe(true); expect(validateName('л')).toBe(false); expect(validateName('㐎')).toBe(false); expect(validateName('ـب')).toBe(false); });
Но этого недостаточно, если вы хотите быть более уверенным. Вы должны охватить больше случаев, таких как:
- Все латинские буквы.
- Максимально другие алфавиты.
- Имена со специальными символами.
- Имена с буквами из смеси алфавитов.
- Все имена в производственной базе данных.
Если вы будете использовать тот же подход к написанию модульных тестов, который мы использовали выше, ваши тесты превратятся в полный беспорядок. Ваш файл будет огромным, и будет непросто определить, что не так с вашей функцией.
import { validateName } from './validator.js'; it('validateName() - should return valid value', () => { // List of all Latin letters expect(validateName('l')).toBe(true); expect(validateName('a')).toBe(true); expect(validateName('c')).toBe(true); ... // List of all accepted symbols expect(validateName('-')).toBe(true); ... // List of all not accepted symbols expect(validateName('*')).toBe(false); ... // All names in the production database expect(validateName('Alex')).toBe(true); expect(validateName('David')).toBe(true); ... // Cyrillic expect(validateName('л')).toBe(false); expect(validateName('Д')).toBe(false); ... // Korean expect(validateName('㐎')).toBe(false); ... // Arabic expect(validateName('ـب')).toBe(false); ... // Names with letters from a mix of alphabets. expect(validateName('Alex-Алекс')).toBe(false); ... });
Рефакторинг
Во-первых, мы можем сгруппировать похожие тесты и дать пояснительные описания. Групповые тесты помогут быстрее определить путь проблемы и предоставить документацию для вашей функции. Новые разработчики легко поймут, что делает эта функция, и будут более уверенно изменять ее.
import { validateName } from './validator.js'; describe('validateName()', () => { it('All latin letters should be accepted.', () => { // List of all Latin letters expect(validateName('l')).toBe(true); expect(validateName('a')).toBe(true); expect(validateName('c')).toBe(true); ... }); it('Some symbols should be accepted.', () => { // List of all accepted symbols expect(validateName('-')).toBe(true); ... }); it('Some symbols should not be accepted.', () => { // List of all not accepted symbols expect(validateName('*')).toBe(false); ... }); it('All names in the production database should be accepted.', () => { // All names in the production database expect(validateName('Alex')).toBe(true); expect(validateName('David')).toBe(true); ... }); it('All korean letters should not be accepted.', () => { // Korean expect(validateName('㐎')).toBe(false); ... }); it('Names with letters from a mix of alphabets should not be accepted.', () => { expect(validateName('Alex-Алекс')).toBe(false); }); })
Чтобы избежать дублирования кода в тестовом файле, мы можем определить новые массивы для всех наших групп символов и букв и пройтись по всем элементам внутри этих массивов, используя forEach
. На этом шаге мы удалили много дубликатов, но в то же время сохранили группировку.
import { validateName } from './validator.js'; const latinLetters = ['a', 'b', 'c', 'd', ...]; const acceptedSymbols = ['-', ...]; const notAcceptedSymbols = ['$', '*', '(', ')', ...]; const cyrilicLetters = ['а', 'б', 'в', 'г', ...]; const koreanLetters = ['ㅊ', 'ㅋ', 'ㅍ', 'ㅎ', ...]; describe('validateName()', () => { describe('All latin letters should be accepted.', () => { latinLetters.forEach((letter) => { it(`Letter ${letter} should be accepted.`, () => { expect(validateName(letter)).toBe(true); }); }); }); describe('Some symbols should be accepted.', () => { acceptedSymbols.forEach((symbol) => { it(`Symbol ${symbol} should be accepted.`, () => { expect(validateName(symbol)).toBe(true); }) }); }); describe('Some symbols should not be accepted.', () => { notAcceptedSymbols.forEach((symbol) => { it(`Symbol ${symbol} should be accepted.`, () => { expect(validateName(symbol)).toBe(false); }) }); }); describe('All cyrilic letters should not be accepted.', () => { cyrilicLetters.forEach((letter) => { it(`Letter ${letter} should not be accepted.`, () => { expect(validateName(letter)).toBe(false); }); }); }); describe('All korean letters should not be accepted.', () => { koreanLetters.forEach((letter) => { it(`Letter ${letter} should not be accepted.`, () => { expect(validateName(letter)).toBe(false); }); }); }); ... })
В качестве бонуса мы помещаем все алфавиты и символы в файлы JSON в специальную папку — __fixtures__
. Таким образом, мы уменьшим размер тестового файла, а разработчики, работающие с этой функцией, будут концентрироваться только на логике при некоторых изменениях.
import { validateName } from './validator.js'; import latinLetters from './__fixtures__/latin-letters.json'; import acceptedSymbols from './__fixtures__/accepted-symbols.json'; import notAcceptedSymbols from './__fixtures__/not-accepted-symbols.json'; import cyrilicLetters from './__fixtures__/cyrilic-letters.json'; import koreanLetters from './__fixtures__/korean-letters.json'; ....
Проделав эти манипуляции, нам удалось охватить множество случаев, наш код не имеет дубликатов, и у нас есть хорошая документация по нашей функции.
Надеюсь, вам понравилась эта статья и вы нашли ее полезной. Это всего лишь несколько примеров того, как мы можем улучшить наши тесты. Но в ближайшее время мы рассмотрим еще примеры. И я надеюсь, что больше разработчиков поймут силу хороших тестов.