Часто ли люди тестируют свои спецификации clojure.spec?

Я изучаю Clojure, сам по себе, и я работал над простым игрушечным проектом по созданию Kakebo (японский инструмент для составления бюджета), который я мог бы изучить. Сначала я буду работать над CLI, затем над API.

С тех пор, как я только начал, я смог "разобраться" со спецификациями, что кажется отличным инструментом в Clojure для проверки. Итак, мои вопросы:

  1. Люди тестируют свои собственные письменные спецификации?
  2. Я тестировал свой как следующий код. Совет, как это исправить?

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

Файл спецификаций:

      (ns kakebo.specs
  (:require [clojure.spec.alpha :as s]))


(s/def ::entry-type #{:income :expense})
(s/def ::expense-type #{:fixed :basic :leisure :culture :extras})
(s/def ::income-type #{:salary :investment :reimbursement})
(s/def ::category-type (s/or ::expense-type ::income-type))
(s/def ::money (s/and double? #(> % 0.0)))
(s/def ::date (java.util.Date.))
(s/def ::item string?)
(s/def ::vendor (s/nilable string?))
(s/def ::entry (s/keys :req [::entry-type ::date ::item ::category-type ::vendor ::money]))

Файл тестов:

      (ns kakebo.specs-test
  (:require [midje.sweet :refer :all]
            [clojure.spec.alpha :as s]
            [kakebo.specs :refer :all]))

(facts "money"
       (fact "bigger than zero"
             (s/valid? :kakebo.specs/money 100.0) => true
             (s/valid? :kakebo.specs/money -10.0) => false)
       (fact "must be double"
             (s/valid? :kakebo.specs/money "foo") => false
             (s/valid? :kakebo.specs/money 1) => false))

(facts "entry types"
       (fact "valid types"
             (s/valid? :kakebo.specs/entry-type :income) => true
             (s/valid? :kakebo.specs/entry-type :expense) => true
             (s/valid? :kakebo.specs/entry-type :fixed) => false))

(facts "expense types"
       (fact "valid types"
             (s/valid? :kakebo.specs/expense-type :fixed) => true))

В качестве последнего последнего вопроса, почему я не могу получить доступ к спецификациям, если попробую выполнить следующий импорт:

      (ns specs-test
  (:require [kakebo.specs :as ks]))

(fact "my-fact" (s/valid? :ks/money 100.0) => true)

1 ответ

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

Есть несколько ошибок в спецификациях:

      ;; this will not work, you probably meant to say the category type 
;; is the union of the expense and income types
(s/def ::category-type (s/or ::expense-type ::income-type))

;; this will not work, you probably meant to check if that the value 
;; is an instance of the Date class
(s/def ::date (java.util.Date.))

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

Например, вы определили как набор других спецификаций:

      (s/def ::entry (s/keys :req [::entry-type ::date ::item ::category-type ::vendor ::money]))

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

      ;; decomplecting the entry types
(def income-entry? #{:income})
(def expense-entry? #{:expense})
(s/def ::entry-type (clojure.set/union expense-entry? income-entry?))

;; decomplecting the category types
(def expense-type? #{:fixed :basic :leisure :culture :extras})
(def income-type? #{:salary :investment :reimbursement})
(s/def ::category-type (clojure.set/union expense-type? income-type?))

(s/def ::money (s/and double? #(> % 0.0)))
(s/def ::date (partial instance? java.util.Date))
(s/def ::item string?)
(s/def ::vendor (s/nilable string?))

(s/def ::expense
  (s/cat ::entry-type expense-entry?
         ::category-type expense-type?))

(s/def ::income
  (s/cat ::entry-type income-entry?
         ::category-type income-type?))

(defn expense-or-income? [m]
  (let [data (map m [::entry-type ::category-type])]
    (or (s/valid? ::expense data)
        (s/valid? ::income data))))

(s/def ::entry
  (s/and
   expense-or-income?
   (s/keys :req [::entry-type ::date ::item
                 ::category-type ::vendor ::money])))

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

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

Другие вопросы по тегам