Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Property-based тестирование JS: Продвинутое тестирование

Property-based тестирование (тестирование, основанное на свойствах) — подход к функциональному тестированию, который помогает проверить, соответствует ли тестируемая функция заданному свойству. Для этого подхода не нужно задавать все тестовые примеры. Его задача — сопоставлять характеристики на выходе с заданным свойством.

Свойство — это утверждение, которое в виде псевдокода можно представить так:

for all (x, y, ...)
such as precondition(x, y, ...) holds
property(x, y, ...) is true

Мы описываем инвариант в стиле «для любых данных, таких, что ... выполняется условие ...» и, в отличие от обычных тестов, не задаем явно все тестовые примеры, а только описываем условия, которым они должны удовлетворять.

Предположим, у нас есть функция divide(), которая находит частное двух чисел.

const divide = (a, b) => a / b;

Напишем обычный тест на эту функцию:

const {equal} = require('assert');

equal(divide(4, 2), 2);
equal(divide(18, 3), 6);

Здесь все просто, передаём на вход числа 18 и 3, ожидаем получить 6. Тесты проходят. Но здесь мы проверили работу функции только на двух парах входных данных. Тест показывает, что функция отрабатывает верно только в этих двух случаях. Но может оказаться так, что при другой паре чисел функция работает неожиданно. Чтобы решить эту проблему, нам нужно написать тест, который сфокусируется не на входных и выходных параметрах, а на свойствах в целом. Эти свойства должны быть истинными для любой правильной реализации.

У операции деления есть свойство: дистрибутивность справа. Оно означает, что деление суммы двух чисел a и b на число c равно сумме a / c + b / c.

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

const a = Math.random() * 1000;
const b = Math.random() * 1000;
const c = Math.random() * 1000;

const left = divide(a + b, c);
const right = divide(a, c) + divide(b, c);

Будем запускать этот тест много раз в цикле, и в определенный момент получим такую комбинацию тестовых данных, когда все три числа равны нулю. Тест упадет, так как деление нуля на ноль даёт NaN, а NaN не равен NaN. Так мы понимаем, что в функцию нужно добавить проверку на нули.

AssertionError [ERR_ASSERTION]: NaN == NaN

Мы написали обычный тест, но использовали в нём не взятые из головы, а произвольные значения и получили возможность выполнять тест много раз на разных входных данных. Таким образом мы проверили саму спецификацию, то есть то, что функция должна делать, а не её поведение в отдельных случаях. Это и есть тестирование на основе свойств — property-based testing.

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

Рассмотрим, в чем заключаются преимущества property-based тестирования:

  • Охватывает все возможные данные. Фреймворки автоматически генерируют данные на основании описанных свойств. В теории эта особенность позволяет охватить все возможные типы входных данных: например, весь диапазон строк или целых чисел.

  • Сокращает тестовый пример в случае сбоя: всякий раз, когда происходит сбой, фреймворк пытается сократить тестовый пример. Например, если условием сбоя является наличие заданного символа в строке, фреймворк должен возвращать строку из одного символа, которая содержит только этот символ. Это серьезное преимущество property-тестирования — в случае сбоя тест прекращает работу на минимальном примере, а не на наборе входных данных.

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

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

Фреймворки

Идея property-тестирования была впервые реализована во фреймворке QuickCheck в языке Haskell. Для JavaScript тоже есть несколько библиотек, одна из них fast-check.

Для её установки нужно выполнить команду:

npm install fast-check --save-dev

Протестируем с её помощью реализацию функции contains(), которая проверяет, содержится ли подстрока в строке. У строк можно выделить два свойства, которые мы можем использовать:

  • Строка всегда содержит саму себя в качестве подстроки
  • Строка a + b + c всегда содержит свою подстроку b, независимо от содержания a, b и c
import fc from 'fast-check';

// Тестируемый код
const contains = (text, pattern) => text.indexOf(pattern) >= 0;

// Описываем свойства

test('string should always contain itself', () => {
  fc.assert(
    fc.property(
      fc.string(),
      text => contains(text, text)
    )
  );
});

test('string should always contain its substring', () => {
  fc.assert(
    fc.property(
      fc.string(), fc.string(), fc.string(),
      (a, b, c) => contains(a + b + c, b)
    )
  );
});

Разберём структуру теста подробнее

fc.assert(<property>(, parameters)) — выполняет тестирование и проверяет, что свойство остаётся верным для всех созданных библиотекой строк a, b и c. Когда происходит сбой, это строка отвечает за сокращение тестового примера для минимального размера, чтобы упростить задачу пользователю. По умолчанию он выполняет проверку свойств по 100 сгенерированным входным данным.

fc.property(<...arbitraries>, <predicate>) — описывает свойство. arbitraries — это значения, которые отвечают за построение входных данных, а predicate — это функция, которая проверяет входные данные. predicate должен либо возвращать логическое значение, либо не возвращать ничего и завершать тест в случае сбоя.

fc.string() — генератор строк, который отвечает за создание и сокращение тестовых значений.

При желании, можно извлечь сгенерированные значения для проверки свойств, заменив fc.assert на fc.sample:

fc.sample(
  fc.property(
    fc.string(), fc.string(), fc.string(),
    (a, b, c) => contains(a + b + c, b)
  )
);

Сгенерированные данные будут выглядеть примерно так:

{a: ") | 2", b: "", c: "$ & RJh %%"}
{a: "\\\" ", b:" Y \\\ "\\\" ", c:" $ S # K3 "}
{a:" $ ", b:" \\\\ cx% wf ", c:" 't4qRA "}
{a:" ", b:" ", c:" n? H. 0% "}
{a:" 6_ # 7 ", b:" b ", c:" 4% E "}
...

Теперь попробуем протестировать заведомо неправильную реализацию функции contains(). Используем её в качестве примера, чтобы показать, что фреймворк генерирует в случае сбоя и как он сокращает ввод:

const contains = (pattern, text) => {
  return text.substr(1).indexOf(pattern) !== -1;
};

Фреймворк генерирует определённый набор данных. Как только тест видит сбой, он запускает процесс сокращения. При тестировании примера, приведённого выше, происходит сбой:

Error: Property failed after 20 tests
{ seed: 1783957873, path: "19:1:0:1:1", endOnFailure: true }
Counterexample: [""," ",""]
Shrunk 4 time(s)
Got error: Property failed by returning false

Заключение

Тестирование на основе свойств — полезный и мощный инструмент. Мы не должны отказываться от классических тестов, но можем их комбинировать с тестированием на основе свойств. Например, можно базовый функционал покрывать классическими тестами на основе примеров, а критически важные функции дополнительно покрывать property-тестами.

Домашнее задание

Загрузите домашнее задание с помощью команды:

hexlet program download frontend-testing-react property-based-testing

Дополнительные материалы

  1. Фреймворк для property-based тестирования

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
900
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
2 февраля 8 месяцев
Иконка программы Node.js-разработчик
Профессия
Разработка бэкенд-компонентов веб-приложений
2 февраля 8 месяцев

Используйте Хекслет по максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»

Изображение Тото

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