Фреймворк тестирования на основе свойств для Node

The Pragmatic Programmer представляет метод тестирования, называемый тестированием на основе свойств, в котором приводится пример на языке Python с использованием фреймворка hypothesis.

Использование hypothesis очень интуитивно понятно и просто, и прекрасно представляет концепцию тестирования на основе свойств. Поэтому я также хотел найти эквивалентную альтернативу в Node. Два из них имеют высокий рейтинг на Github: JSVerify с 1,6 тыс. звезд и fast-check с 2,8 тыс. звезд. Поэтому я выделил некоторое время, чтобы немного изучить fast-check и попытаться приблизить его к моей повседневной работе.

Эта статья является кратким обзором и простым примером для документирования опыта.

Почему тестирование на основе свойств?

Прежде чем приводить примеры, давайте объясним, почему мы используем тесты на основе свойств. На самом деле, мне не нравится термин «имущественные». Я бы сказал «чрезвычайно объемное» тестирование.

Все мы знаем, что пирамида тестирования выглядит следующим образом.

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

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

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

На данном этапе мы хотели бы, чтобы фреймворк автоматически генерировал достаточное количество сценариев (разумных или нет) для проверки написанного нами кода, а написанные нами тестовые случаи должны лишь обеспечивать корректность их «свойств». Это и есть истоки тестирования на основе свойств.

Тем не менее

Реальность такова, что интеграционное тестирование — это примерно то же самое, что и модульное тестирование.

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

Для большинства разработчиков правильно разделить модульное тестирование и интеграционное тестирование — непростая задача. Чтобы иметь возможность полностью разделить тестовые случаи, им необходимо обладать навыками паттернов проектирования, инъекции зависимостей, инверсии зависимостей и т.д., чтобы сделать это хорошо. Поэтому большинство тестовых сред основаны на конкретном тестовом окружении, например, использование docker-compose для создания одноразовой базы данных и тестовых данных и тестирования на ней.

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

В общем, мне нужно несколько способностей.

  1. Уметь тестировать async/await.
  2. Уметь проверять больше контекстов, таких как assertEqual.

Введение в fast-check

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

Сначала представим структуру fast-check.

  • Утверждение (fc.assert)
  • Свойства (fc.property или fc.asyncProperty)

Функция fc.assert заключается в проверке корректности всех тестов, автоматически генерируемых свойствами. Свойства необходимы для описания двух важных блоков.

  • Runner
  • Arbitraries

Runner — это тестируемый контекст, т.е. цель. С другой стороны, арбитражи — это входные параметры цели, которые автоматически генерируются свойствами, и все, что нам нужно сделать, это задать для них правила, например, только целые числа.

Ниже приведен простой пример.

fc.assert(
  fc.property(fc.integer(), fc.integer(), (i, j) => {
    return i + j === add(i, j);
  })
);
Войти в полноэкранный режим Выход из полноэкранного режима

Два fc.integer() являются арбитражами, а последующая анонимная функция — бегунком, который принимает два аргумента i и j, соответствующие предыдущим арбитражам. Мы хотим проверить, действительно ли функция add правильно суммирует два аргумента, так что результат add должен соответствовать +.

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

  1. Хотя нашей целью тестирования является add, но хорошая интеграция с некоторыми условиями в бегунке может сделать не только эффект boolean.

Примеры быстрой проверки

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

идентификатор пользователя деньги
123 100
456 200
абк 9999
def 0

Существует функция async function getMoney(limit), которая будет сортировать деньги в порядке возрастания, а также определять, сколько денег вернуть, основываясь на параметрах.

Теперь мы хотим протестировать этот «черный ящик».

describe("fast-check test", () => {
  before(async () => {
      // generate 10 random records
  });

  it("#1", async () => {
    const result = await getMoney(100);
    expect(result.length).to.be.equal(10);
  });

  it("#2", async () => {
    await fc.assert(
      fc.asyncProperty(fc.integer(), async (i) => {
        const result = await getMoney(i);
        return result.length <= 10 && result.length >= 0;
      })
    );
  });

  it("#3", async () => {
    await fc.assert(
      fc.asyncProperty(fc.integer({ min: 0, max: 10 }), async (i) => {
        const result = await getMoney(i);
        return result.length === i;
      })
    );
  });

  it("#4", async () => {
    await fc.assert(
      fc.asyncProperty(fc.integer(), async (i) => {
        const result = await getMoney(i);
        if (result.length > 1) {
          let prev = parseFloat(result[0]);
          for (let i = 1; i < result.length; i++) {
            const curr = parseFloat(result[i]);
            if (curr < prev) {
              return false;
            }
            prev = curr;
          }
        }
        return true;
      })
    );
  });
});
Войдите в полноэкранный режим Выход из полноэкранного режима

Позвольте мне объяснить вкратце.

  1. Просто проверьте, что функция действительно работает, здесь не используется fast-check.
  2. Учитывая произвольное целое число, длина возвращаемого результата должна быть между 0 и 10, потому что мы создали только десять записей в before.
  3. Учитывая диапазон целых чисел, длина возвращаемого результата должна быть равна заданной длине.
  4. Убедитесь, что порядок всего массива действительно является возрастающим. Из этого бегунка видно, что даже очень сложные условия могут быть проверены, но будьте осторожны, чтобы не допустить ошибок в тестовом примере, приводящих к необходимости проверки тестового примера.

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

Контрпример: [-1234567890]

Это означает, что тест не прошел, когда i = -1234567890. Возможно, отрицательное число обрабатывается неправильно или «большое» отрицательное число обрабатывается неправильно. Это самое время написать настоящий модульный тест (или интеграционный тест) и проверить -1234567890, чтобы потом использовать такой неудачный случай в качестве регрессионного теста.

Заключение

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

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

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

Оцените статью
Procodings.ru
Добавить комментарий