Идея упражнений return true to win состоит в том, чтобы создать значение, подобное тому, которое данная функция возвращает true. Значения могут быть любого типа, но также могут быть ограничены объектами, массивами, функциями в зависимости от постановки задачи. В этой статье основное внимание уделяется применению тестирования на основе свойств для решения таких проблем.

Примеры таких головоломок доступны по ссылке: https://alf.nu/ReturnTrue

Доказательство концепции на основе фреймворка fast-check доступно по адресу https://github.com/dubzzz/breaking-return-true-to-win/

Тестирование на основе свойств

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

Given inputs satisfying a pre-requisite,
The statement is true

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

Приложение для возврата true, чтобы выиграть

Операторы Return true to win можно легко переключить на оператор тестирования на основе свойств.

Возьмем следующий пример:

Найдите выражение JavaScript, например: x == !x

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

Выполнение

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

function negationEqualsOriginal(x) {
    return x == !x;
}
fc.assert(
    fc.property(
        fc.anything(),
        x => ! negationEqualsOriginal(x)
    )
);

Давайте взглянем на эту реализацию поближе:

  • fc.assert отвечает за многократное выполнение свойства в попытке проверить возможные проблемы. Он также обрабатывает сжимающую часть, чтобы выводить значения как можно меньше (нет необходимости иметь объект с сотнями ключей, если один ключ приводит к сбою)
  • fc.property объявляет свойство с точки зрения того, что оно принимает на вход и как оценить, что оно работает правильно
  • fc.anything() — это произвольный объект, он отвечает за создание любого возможного объекта или примитива. Мы можем использовать несколько произвольных значений для одного и того же свойства, просто помещая их сразу после первого. Подробнее об этом смотрите в документации быстрой проверки
  • x => ! negationEqualsOriginal(x) — это предикат, который фреймворк ожидает видеть всегда истинным. В нашем случае мы хотим, чтобы он нашел случай, когда он становится ложным

А как насчет сокращения?

В приведенном выше примере оба [[[[undefined]]]] == ![[[[undefined]]]], [undefined] == ![undefined] или [] == ![] равны true, но из-за сжатия фреймворк предоставит нам только минимальный ошибочный пример, который в этом случае будет пустым массивом.

Идти дальше

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

const wrapIt = function(fn) {
    return (...args) => {
        try { return !fn(...args); }
        catch (err) { return true; }
    };
};

И замените x => ! negationEqualsOriginal(x) на x => wrapIt(negationEqualsOriginal)(x).

Вывод

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

Доказательство концепции доступно на https://github.com/dubzzz/breaking-return-true-to-win.