Сегодня я собираюсь обсудить модульные тесты и способы их улучшения. Мы рассмотрим несколько примеров улучшения модульных тестов и удаления дубликатов в коде. Для тестирования я буду использовать 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);
});

Но этого недостаточно, если вы хотите быть более уверенным. Вы должны охватить больше случаев, таких как:

  1. Все латинские буквы.
  2. Максимально другие алфавиты.
  3. Имена со специальными символами.
  4. Имена с буквами из смеси алфавитов.
  5. Все имена в производственной базе данных.

Если вы будете использовать тот же подход к написанию модульных тестов, который мы использовали выше, ваши тесты превратятся в полный беспорядок. Ваш файл будет огромным, и будет непросто определить, что не так с вашей функцией.

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';

....

Проделав эти манипуляции, нам удалось охватить множество случаев, наш код не имеет дубликатов, и у нас есть хорошая документация по нашей функции.

Надеюсь, вам понравилась эта статья и вы нашли ее полезной. Это всего лишь несколько примеров того, как мы можем улучшить наши тесты. Но в ближайшее время мы рассмотрим еще примеры. И я надеюсь, что больше разработчиков поймут силу хороших тестов.