Почитал с утра пару статей по delta debugging. Не могу теперь выкинуть из головы идею.

Простой и автоматический поиск ошибок

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

Однако, представим, что у нас есть некая библиотека. Мы говорим ей “протестируй-ка нам класс GodlikeEnterpriseStaffController, вот тебе ссылка на объект”. Та что-то шурудит у себя в кишках, а потом выдаёт примеры тест-кейсов типа:

Вызываем метод GodlikeEnterpriseStaffController.createFoo()
 * тот создаёт файл new File(...)
 * создание файла падает с исключением из-за отсутствия прав доступа
 * метод createFoo() падает с неожиданным IOException, хотя не должен

Как бы такая библиотека работала под капотом?

Реализация

Используя Reflection, мы можем получить список методов класса, а также их параметры. Используя этот набор данных, библиотека может сгенерировать произвольную цепочку вызовов к данным методам, используя произвольные параметры. Если во время вызова с классом происходит что-то непотребное, то данный набор вызовов обрабатывается с помощью delta debugging, чтобы превратиться в минимальный падающий тест-кейс. Который и показывается пользователю.

Однако тут возникает вопрос - что делать с зависимостями? Как правило, классы не живут в изоляции (а наш GodlikeEnterpriseStaffController тем более). Что делать с различными обращениями к зависимостям?

Ответ прост и брутален: все зависимости заворачиваются в моки. Для каждого метода мы снова можем определить через Reflection набор параметров и возвращаемые значения - и снова генерируем для них произвольные возвращаемые значения. В итоге, когда тестируемый класс делает вызовы других классов, то он получает полный ушат случайных значений в ответ. Либо, если в сигнатуре вызваемого метода прописана возможность выкинуть исключение, получает это исключение. Либо что-то ещё неприятное.

Профит

Широко известно, что многим программистам сложно продумать все возможные пути исполнения их кода и то, как повлияют на поведение кода все возможные входные параметры. Автоматическая генерация подобных параметров помогла бы находить неожиданное поведение кода в полу-автоматическом режиме. Создаваемые системой “тест-кейсы” можно было бы либо превращать в реальные юнит-тесты, либо играть с генератором в пинг-понг (сквош?), на ходу исправляя в коде находимые компьютером проблемы.

Более того, такой подход, на первый взгляд, намного привлекательней, чем обычный TDD или, тем более, property-driven testing. Программисту не нужно ни придумывать тест-кейсы, ни формулировать инварианты - просто брать и скармливать тест системе. Шикарно ведь, правда?

Почему это не будет работать

В теории это всё хорошо, но на практике реализация подобной системы, скорее всего, столкнётся с рядом серьёзных вызовов.

  • Невозможность проверять бизнес-логику. Конечно, с помощью такой системы можно достаточно легко находить всякие NullPointerException, но такие находки могут оказаться максимумом, на что она способна.
  • Нереалистичные тест-кейсы. Созданные системой кейсы могут не отражать реальность, в которой выполняется код. Например, не учитывать, что одна из завимостей тестируемого класса является синглтоном (два разных обращения к зависимости создадут два разных мока). Или не учитывать неявный контракт класса: в метод передаётся интерфейс, который внутри кастуется в конкретную реализацию этого интерфейса (да, это bad practice само по себе; однако без учёта подобной специфики мы упадём на первой строчке тестируемого метода с ClassCastException, а с учётом смогли бы пройти дальше). Как результат, все генерируемые тест-кейсы малополезны для реального применения.
  • Возможно, какие-то зависимости чисто технически сложно превращать в моки.

Конечно, несложно придумать способы борьбы с такими проблемами. Например, мы можем добавить настройку окружения, в которой прописываем некоторые предположения относительно того, как следует генерировать варианты для зависимостей. Или указывать, что надо тестировать не один класс в изоляции, а некоторую группу классов (т.е., не заменять часть зависимостей на моки). Однако всё это компрометирует изначальную идею “просто скормил класс системе и получил результат”.

Итого

Я подозреваю, что идея далеко не нова, и кто-то её уже давно высказывал. Возможно, даже какая-то из существующих систем работает подобным образом (какой-нибудь Quviq QuickCheck или Conjecture; трудно об этом судить, не попробовав их в деле). Всё, что у меня пока что есть, - это мысленный эксперимент. Пускай пока в таком виде и остаётся.



Published

10 December 2015

Tags