Wishful thinking

В документации по Midje есть вводная статья о top-down development. Именно в ней показывается, что библиотека Midje подходит для разработки на Clojure как нельзя лучше.

Идею wishful thinking предложили в [1] уже несколько десятилетий назад, в подробностях алгоритм разработки в стиле wishful thinking можно найти в [2]. Смысл идеи состоит в следующем. Предположим, нам необходимо решить какую-то задачу в некоторой предметной области. Традиционный подход состоит в том, чтобы выполнить декомпозицию задачи на подзадачи, а те – на другие подзадачи до тех пор, пока все подзадачи не станут досаточно простыми для реализации. Wishful thinking идет немного иным путем. Мы представляем, что уже есть некоторый API или предметно-ориентированный язык, моделирующий предметную область. И, представив, что этот API уже есть, решаем с его помощью задачу. Затем – реализуем все несуществующие функции этого API.

Midje позволяет смоделировать этот несуществующий API и отложить его реализацию на самый последний этап разработки. Например, для реализации some-func нам необходимы функции из несуществующего API: api-func1 и api-func2.

На заметку: указанные функции могут быть не реализованы, но объявить их всё же нужно.

Мы пишем тело some-func так, будто api-func1 и api-func2 уже готовы, а затем пишем такой тест:

(facts "факты о функции 'some-func'"
    (fact "'some-func' работает на несуществующем API"
    
     (some-func 123 "arg2") => 42

     (provided
         (api-func1 123) => nil
         (api-func2 "arg2") => 42 ))
  )

Обратите внимание на секцию provided. Именно там мы определяем, как должны вести себя нереализованные api-func1 и api-func2. Мы указываем, на каких аргументах вызываются эти функции и что они должны вернуть.

Таким образом, мы можем вначале реализовать some-func, и только потом – api-func1 и api-func2.

Фиктивные функции и значения

Показанный выше способ определения фиктивных функций работает в точности, как известный Java-фреймворк Mockito. Но, помимо фиктивных функций Midje позволяет подставлять и фиктивные значения.

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

(facts "факты о функции 'some-func'"
    (fact "'some-func' работает на несуществующем API"
    
     (some-func 123 ..ARG2..) => ..RESULT..

     (provided
         (api-func1 anything) => nil
         (api-func2 ..ARG2..) => ..RESULT.. ))
  )

Здесь anything означает любой, неважно какой аргумент. ..ARG2.. и ..RESULT.. – метазначения. Их нужно использовать тогда, когда не важно содержимое, а важно только, чтобы оно было одинаковым везде, где используется одинаковое название метазначения.

Некоторые особенности

  • Midje предлагает очень удобный механизм автотестов. В REPL нужно сделать следующее:
(require '[midje.repl :refer [autotest]])
 (autotest)

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

Особенность здесь вот какая: автотесты работают не всегда предсказуемо, если “нахимичить” с переопределениями var-ов. Лечение здесь одно: перезагрузка всех “сломанных” неймспейсов либо использовать lein midje из консоли.

  • Нетривиальной задачей является тестирование макросов, если их нужно поместить в секцию provided. Дело в том, что Midje не принимает макросы в provided. Поэтому, единственный способ – это раскрыть макрос macroexpand-ом и подставить в provided тело макроса. Так, например, обычный (taoensso.timbre/debug “hello”) в секции provided будет выглядеть так: (taoensso.timbre/-log! anything anything anything anything anything anything anything anything anything anything). К сожалению, Midje не позволяет “сократить” все десять anything до какого-нибудь одного параметра.

  • Если необходимо протестировать асинхронную функцию, то следует воспользоваться макросом with-redefs, чтобы переопределить её на что-то синхронное. Макрос with-redefs не переопределяет другие макросы, поэтому и здесь нужно раскрывать их macroexpand-ом:

(with-redefs [ future-call (fn [func] (func)) ]
   (future (+ 1 2))
 )

Точно также можно “нейтрализовать” log-и, чтобы они не отображались в выводе из тестов.

(with-redefs [ log/-log! (fn [& args] nil) ]
   (log/debug "hello!")
 )

Выводы

Midje отлично дополняет REPL-ориентированный подход Clojure. Если необходимо на скорую руку сделать черновик функционала, чтобы приблизительно представить возможную архитектуру, Midje предоставляет возможность использовать фиктивные функции и значения.

Литература

1. Абельсон Х., Сассман Дж. 2.1 Введение в абстракцию данных. В кн.: Структура и интерпретация компьютерных программ, Москва, Добросвет, 2010, с.93.
2. Freeman S., Pryce N. Support for TDD with Mock Objects. In: Growing Object-Oriented Software, Guided by Tests. Location: Addison-Wesley, 2010, pp. 19-20.