Если вы читаете мой блог, вы, вероятно, слышали, что «код — это данные, данные — это код», и однажды вы посмотрели на гомоничность . Возможно, вы глубоко поняли эту идею, когда впервые услышали ее; Я определенно не сделал. Однако недавнее дополнение к ожиданиям открыло мне глаза на то, насколько мощным может быть это свойство языка программирования.
Я начну с признания того, что я слышал, когда впервые столкнулся с гомойконичностью. Стюарт Хэллоуэй начал продвигать Clojure, и гомойконичность была одним из преимуществ, которые он отметил. Я зашел на страницу википедии, переварил слова «код — это данные, данные — это код» и подумал про себя: ну да, очевидно. Я потратил много времени на работу с DSL в Ruby, и у меня был большой опыт оценки кода в разных контекстах. Я подумал что-то вроде: «Итак, вы фиксируете код как данные и оцениваете его везде, где это имеет смысл, я не вижу в этом ничего особенного. Короче я не понял.
Перенесемся на несколько лет и несколько часов на полную разработку Clojure, и вы увидите, что я добавляю тестирование на основе взаимодействия к ожиданиям, То, что я имел в виду для тестирования взаимодействий, было простым, я хочу написать для теста то же самое, что и для производственного кода. Кроме того, я хочу, чтобы формат теста соответствовал тому же формату, который используется для тестирования на основе состояния: как (expect expected actual)
только у меня появилось четкое представление о моих требованиях, формат тестов стал легко визуализироваться.
Предположим, у меня есть функция, которая печатает в стандартном формате, и я хочу проверить, что эта печать происходит.
(defn print-it [it] (println it)) ;;; the test needs to be in the form (expect expected actual), ;;; and the line we're testing is (println it) ;;; so you could envision a test similar to the one below (expect (println 5) (print-it 5))
Вышеприведенный тест выглядит великолепно, но (println 5)
будет оценен, вернет ноль и будет использовать ноль в качестве ожидаемого значения. Мне нужен был какой-то способ, чтобы программист мог сказать структуре тестирования, что это тест взаимодействия, и ожидания должны были подтвердить, что функция была вызвана с указанными параметрами. Попробовав несколько разных форматов, я остановился на следующем решении.
(defn print-it [it] (println it)) ;;; the test needs to be in the form (expect expected actual), ;;; and the line we're testing is (println it) ;;; so you could envision a test similar to the one below (expect (interaction (println 5)) (print-it 5))
Завершив взаимодействие, которое я хотел проверить (interaction ...)
, я создал простой способ идентифицировать и зафиксировать функцию и аргументы, которые необходимо проверить.
Как только я определился с синтаксисом, я решил добавить поддержку ожиданий. Если вы углубитесь в реализацию ожиданий, то обнаружите, что ожидаемый — это макрос, который делегирует обработку «ожидаемых» и «фактических» аргументов макросу doexpect. Первое, что делает макрос doexpect, это проверяет, является ли ожидаемый список, и (если так), если первый аргумент является символом «взаимодействие» ( источник здесь). Если первый аргумент не является списком, который начинается с ‘взаимодействия, то данные передаются в do-value-ожидаемое и расширяются более или менее как есть. Однако, если первый аргумент является списком, который начинается с ‘взаимодействия, то данные передаются в ожидание дел-взаимодействия, а ожидание-взаимодействия затем разрушает данные, захватывая только те части списка, которые ему нужны ( источник здесь ). Когда я написал этот код, я нашел его очень интересным.
Когда я предполагал синтаксис взаимодействия, я предполагал, что(interaction ...)
будет вызовом макроса, и мне нужно будет манипулировать данными, передаваемыми во взаимодействие. Однако, как только я вошел в реальную реализацию, я обнаружил, что использую символ «взаимодействие», но никогда не определяю макрос или даже функцию. Вот когда гомоконичность действительно стала мне понятна. Я написал код, который, я был уверен, нуждался в реализации, но он использовался исключительно как данные.
Если вы продолжите копаться в этом примере, вы обнаружите, что все, что находится внутри(interaction ...)
никогда не используется так, как написано, но вместо этого расширяется таким образом, чтобы ожидания могли повторно привязать указанную функцию и использовать ожидаемые аргументы во время проверки. В результате вы пишете тот же самый код таким же образом, но в вашем тесте он используется исключительно как данные, а в вашем рабочем коде он используется исключительно как код. Я большой поклонник соглашения, и нет лучшего соглашения, чем «использовать одно и то же».
Позже я добавил возможность добавлять тесты взаимодействия для вызовов к объектам Java, что привело к следующему поведению ожиданий.
- Если ожидаемое значение не является взаимодействием, оно будет расширено как есть.
- Если ожидаемое значение является взаимодействием с функцией Clojure, оно будет использоваться исключительно в качестве данных и расширено для повторного связывания функции, захвата всех вызовов функции и проверки того, что вызов произошел с указанными вами аргументами.
- Если ожидаемое значение является взаимодействием с методом Java, оно будет использоваться исключительно в качестве данных и расширено до кода настройки и проверки mockito .
Таким образом, ожидаемое значение иногда является кодом, а иногда данными.