Property-based testing

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

Преимущества property-based testing

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

Интеграция с Jest

Jest — это популярный фреймворк для тестирования JavaScript, который по умолчанию предоставляет поддержку тестов с использованием утверждений (assertions). В дополнение к стандартному функционалу, Jest может быть использован для property-based testing с помощью сторонних библиотек, таких как fast-check.

Fast-check — это популярная библиотека для property-based testing в JavaScript. Она интегрируется с Jest и позволяет создавать более сложные и разнообразные тесты, генерируя случайные входные данные, которые могут быть использованы для проверки корректности работы функций.

Установка fast-check

Чтобы использовать fast-check с Jest, необходимо сначала установить библиотеку:

npm install fast-check

После этого можно подключить её в тестах.

Основы использования fast-check

В fast-check тесты описываются через properties — абстрактные утверждения о поведении функции или системы. Для создания property-based теста достаточно указать входные данные (генераторы) и саму функцию, которую нужно протестировать.

Пример использования:

import * as fc from 'fast-check';

test('функция сложения коммутативна', () => {
  fc.assert(
    fc.property(fc.integer(), fc.integer(), (a, b) => {
      return a + b === b + a;
    })
  );
});

В этом примере создается тест, проверяющий свойство коммутативности для операции сложения. Библиотека fast-check автоматически генерирует случайные целые числа для a и b, и проверяет, что свойство (a + b == b + a) выполняется для всех комбинаций значений.

Генераторы данных

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

  • fc.integer() — генератор случайных целых чисел.
  • fc.string() — генератор случайных строк.
  • fc.boolean() — генератор случайных булевых значений.
  • fc.array(fc.integer()) — генератор массива целых чисел.
  • fc.tuple(fc.integer(), fc.string()) — генератор кортежа из целого числа и строки.

Генераторы могут быть комбинированы для создания более сложных структур данных. Например:

fc.array(fc.tuple(fc.integer(), fc.string()))

Этот генератор будет создавать массив из кортежей, каждый из которых будет содержать целое число и строку.

Контроль над генерацией данных

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

  • Количественные ограничения — можно задать максимальное количество попыток для одного теста. Это полезно для сокращения времени тестирования.
fc.assert(
  fc.property(fc.integer(), fc.integer(), (a, b) => a + b === b + a),
  { numRuns: 1000 }
);
  • Генерация данных с ограничениями — можно задать диапазоны для значений, которые генерируются. Например, можно ограничить целые числа диапазоном от 0 до 1000.
fc.assert(
  fc.property(fc.integer({ min: 0, max: 1000 }), fc.integer(), (a, b) => a + b === b + a)
);

Отчёты о сбоях

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

Пример отчета:

Test failed on attempt 12 with the following input:
a: 354
b: 742
Expected: 354 + 742 == 742 + 354
Got: 1096 != 1096

Такой отчет позволяет точно понять, какие значения вызвали ошибку, и быстро локализовать проблему.

Генерация сложных данных

Для более сложных сценариев, например, когда данные имеют определенные структуры, можно создавать свои собственные генераторы. Например, если требуется протестировать объект с определёнными свойствами, можно написать кастомный генератор для такого объекта.

Пример генератора объекта:

const userGenerator = fc.record({
  name: fc.string(),
  age: fc.integer({ min: 18, max: 99 }),
});

test('пользователь имеет правильные данные', () => {
  fc.assert(
    fc.property(userGenerator, (user) => {
      expect(user.age).toBeGreaterThanOrEqual(18);
      expect(user.age).toBeLessThanOrEqual(99);
      expect(user.name).toBeTruthy();
    })
  );
});

Здесь используется fc.record() для создания генератора объектов с полями name и age, и затем проверяется, что значения этих полей соответствуют определённым ограничениям.

Совмещение с обычными тестами Jest

fast-check прекрасно интегрируется с Jest, позволяя использовать его в качестве дополнения к стандартным тестам. Тесты, использующие fast-check, выглядят как обычные Jest-тесты, и их можно комбинировать с обычными unit-тестами.

Пример комбинирования:

test('проверка суммы положительных чисел', () => {
  fc.assert(
    fc.property(fc.integer({ min: 0 }), fc.integer({ min: 0 }), (a, b) => {
      expect(a + b).toBeGreaterThanOrEqual(0);
    })
  );
});

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

Заключение

Использование property-based testing с Jest через библиотеку fast-check позволяет создавать более гибкие и обширные тесты, которые могут охватывать большее количество возможных случаев. Это особенно полезно при тестировании функций, которые могут работать с большим количеством возможных входных данных, где важно проверить корректность работы программы на различных комбинациях значений. Такой подход значительно улучшает надежность кода и помогает обнаружить ошибки, которые могли бы остаться незамеченными при традиционном тестировании.