Let's say we have API to save different type of files' attributes into DB*: text, images, audio and video files. It should be able to get the following fields based on their type:
base properties for all the files:
{"file-type": "text",
"location": "/Documents",
"creation-time": "2020-03-02",
"name": "sometext.txt"}
additional props specific only for some types:
text: only base props
video file: base props + {"duration(s)":123, "resolution":"4k"}
audio file: base props + {"duration(s)":123}
image: base props + {"resolution":"2048x1536"}
as we can see, some fields should be nil depenping on "file-type". Let's say we need to validate this kind of input, which it can be any type of the described. So the specs are:
(s/def ::file-type (s/with-gen (s/and string? not-empty) #(s/gen #{"text" "image" "video" "audio"})))
(s/def ::location (s/with-gen (s/and string? not-empty) #(s/gen #{"/Documents"})))
(s/def ::creation-time (s/with-gen (s/and string? not-empty) #(s/gen #{"2020-03-02"}))) ;for simplicity
(s/def ::name (s/with-gen (s/and string? not-empty) #(s/gen #{"sometext.txt" "image.jpg" "video.mp4" "audio.mp3"}))) ;for simplicity
(s/def ::duration (s/and int? not-empty)) ;for simplicity
(s/def ::resolution (s/with-gen (s/and string? not-empty) #(s/gen #{"4k" "2048x1536"}))) ;for simplicity
(s/def ::files-input (s/keys :req-un [::file-type ::location ::creation-time ::extension] :opt-un [::duration ::resolution]))
Let's also say that there is a programmatic validator that checks the right field set was passed for each file type (e.g., "video" has "duration" field, but "text" has not).
The question is: how to generate the full ready-made input with those dependencies for unit-tests (including the dependant fields)?
*(let's leave aside the question if it is a right design for API as this example is not from real life and for demonstration purpose only)
Based on multi-spec doc, we have to add separate method with its own set of fields for each case:
(s/def ::file-type (s/with-gen (s/and string? not-empty) #(s/gen #{"text" "image" "video" "audio"})))
(s/def ::location (s/with-gen (s/and string? not-empty) #(s/gen #{"/Documents"})))
(s/def ::creation-time (s/with-gen (s/and string? not-empty) #(s/gen #{"2020-03-02"}))) ;for simplicity
(s/def ::name (s/with-gen (s/and string? not-empty) #(s/gen #{"sometext.txt" "image.jpg" "video.mp4" "audio.mp3"}))) ;for simplicity
(s/def ::duration pos-int?) ;for simplicity
(s/def ::resolution (s/with-gen (s/and string? not-empty) #(s/gen #{"4k" "2048x1536"}))) ;for simplicity
(s/def ::base-props (s/keys :req-un [::file-type ::location ::creation-time ::name]))
(s/def ::file-type-key (s/with-gen keyword? #(s/gen #{:text :image :video :audio})))
(defmulti file :file-type-key)
(defmethod file :text [_]
(s/merge (s/keys :req-un [::file-type-key]) ::base-props))
(defmethod file :image [_]
(s/merge (s/keys :req-un [::file-type-key ::resolution]) ::base-props))
(defmethod file :video [_]
(s/merge (s/keys :req-un [::file-type-key ::duration ::resolution]) ::base-props))
(defmethod file :audio [_]
(s/merge (s/keys :req-un [::file-type-key ::duration]) ::base-props))
(defmethod file :default [_]
(s/merge (s/keys :req-un [::file-type-key]) ::base-props))
(s/def ::file-input (s/and (s/multi-spec file :file-type-key)
(fn [{:keys [file-type file-type-key]}]
(= file-type-key (keyword file-type)))))
will give generated input for unit-test (checkable by stest/check
):
(gen/sample (s/gen ::file-input) 2)
=>
({:file-type-key :text, :file-type "text", :location "/Documents", :creation-time "2020-03-02", :name "sometext.txt"}
{:file-type-key :image,
:resolution "4k",
:file-type "image",
:location "/Documents",
:creation-time "2020-03-02",
:name "sometext.txt"})
we had to add one more field ::file-type-key
as a selector (has to be a keyword), but it does not affect tests and can be dissoc
ed easily.