Introduction
A quick overview of Specmonstah's purpose and a short sweet working example.

Purpose

Specmonstah (Boston for "Specmonster") lets you write test fixtures that are clear, concise, and easy to maintain. It's great for dramatically reducing test boilerplate.
Say you want to test a scenario where a forum post has gotten three likes by three different users. You'd first have to create a hierarchy of records for the post, topic, topic category, and users. You have to make sure that all the foreign keys are correct (e.g. the post's :topic-id is set to the topic's :id) and that everything is inserted in the right order.
With Specmonstah, all you have to do is write code like this:
1
(insert {:like [[3]]})
Copied!
and these records get inserted in a database (in the order displayed):
1
[[:user {:id 1 :username "T2TD3pAB79X5"}]
2
[:user {:id 2 :username "ziJ9GnvNMOHcaUz"}]
3
[:topic-category {:id 3 :created-by-id 2 :updated-by-id 2}]
4
[:topic {:id 6
5
:topic-category-id 3
6
:title "4juV71q9Ih9eE1"
7
:created-by-id 2
8
:updated-by-id 2}]
9
[:post {:id 10 :topic-id 6 :created-by-id 2 :updated-by-id 2}]
10
[:like {:id 14 :post-id 10 :created-by-id 1}]
11
[:like {:id 17 :post-id 10 :created-by-id 2}]
12
[:user {:id 20 :username "b73Ts5BoO"}]
13
[:like {:id 21 :post-id 10 :created-by-id 20}]]
Copied!
Without Specmonstah, you'd have to write something like this to achieve the same result:
1
(let [user-1 (create-user! {:username "u1" :email "[email protected]"})
2
user-2 (create-user! {:username "u2" :email "[email protected]"})
3
user-3 (create-user! {:username "u3" :email "[email protected]"})
4
tc (create-topic-category! {:created-by-id (:id user-1), :updated-by-id (:id user-1)})
5
t (create-topic! {:title "topic"
6
:created-by-id (:id user-1)
7
:updated-by-id (:id user-1)
8
:topic-category-id (:id tc)})
9
p (create-post! {:topic-id (:id t), :created-by-id, (:id user-1), :updated-by-id (:id user-1)})]
10
(create-like! {:user-id (:id user-1) :post-id (:id p)})
11
(create-like! {:user-id (:id user-2) :post-id (:id p)})
12
(create-like! {:user-id (:id user-3) :post-id (:id p)}))
Copied!
Call me crazy, but I think (insert {:like [[3]]}) is better. The Specmonstah DSL communicates what's important about the scenario you're trying to test. It eliminates all the visual noise that results from having to type out the foreign key relationships. It's:
    Clear - the intention of the code is not obscured by boilerplate
    Concise - a compact DSL lets you elegantly express the data you need to work with in your test
    Easy to maintain - Less code = less bugs. In the non-Specmonstah example you can imagine how easy it would be to introduce a typo or otherwise make a mistake that's hard to figure out
If you think writing code that's clear, concise, and easy to maintain is super cool 😎then read on! The next section will give you an example you can play with in the REPL. This guide also includes:
    An infomercial that highlights more Specmonstah's cool features
    A thorough tutorial that walks you through how to use Specmonstah

Short Sweet Example

We've got a big ol' tutorial to help you master Specmonstah, but if you're more the gimme fun now kind of person, then try out this little interactive example. First, clone Specmonstah:
1
git clone https://github.com/reifyhealth/specmonstah.git
Copied!
Open examples/short-sweet/short_sweet.clj in your favorite editor and start a REPL. I've also included the code below in case for example you don't have access to a REPL because, say, you're in some kind of Taken situation and you only have access to a phone and you're using your precious battery life to go through this guide.
The first ~66 lines of code include all the setup necessary for the examples to run, followed by snippets to try out with example output. Definitely play with the snippets πŸ˜€ Can you generate multiple todos or todo lists?
1
(ns short-sweet
2
(:require [reifyhealth.specmonstah.core :as sm]
3
[reifyhealth.specmonstah.spec-gen :as sg]
4
[clojure.spec.alpha :as s]
5
[clojure.spec.gen.alpha :as gen]))
6
​
7
;;-------*****--------
8
;; Begin example setup
9
;;-------*****--------
10
​
11
;; ---
12
;; Define specs for our domain entities
13
​
14
;; The ::id should be a positive int, and to generate it we increment
15
;; the number stored in `id-seq`. This ensures unique ids and produces
16
;; values that are easier for humans to understand
17
(def id-seq (atom 0))
18
(s/def ::id (s/with-gen pos-int? #(gen/fmap (fn [_] (swap! id-seq inc)) (gen/return nil))))
19
(s/def ::not-empty-string (s/and string? not-empty #(< (count %) 10)))
20
​
21
(s/def ::username ::not-empty-string)
22
(s/def ::user (s/keys :req-un [::id ::username]))
23
​
24
(s/def ::created-by-id ::id)
25
(s/def ::content ::not-empty-string)
26
(s/def ::post (s/keys :req-un [::id ::created-by-id ::content]))
27
​
28
(s/def ::post-id ::id)
29
(s/def ::like (s/keys :req-un [::id ::post-id ::created-by-id]))
30
​
31
;; ---
32
;; The schema defines specmonstah `ent-types`, which roughly
33
;; correspond to db tables. It also defines the `:spec` for generting
34
;; ents of that type, and defines ent `relations` that specify how
35
;; ents reference each other
36
(def schema
37
{:user {:prefix :u
38
:spec ::user}
39
:post {:prefix :p
40
:spec ::post
41
:relations {:created-by-id [:user :id]}}
42
:like {:prefix :l
43
:spec ::like
44
:relations {:post-id [:post :id]
45
:created-by-id [:user :id]}
46
:constraints {:created-by-id #{:uniq}}}})
47
​
48
;; Our "db" is a vector of inserted records we can use to show that
49
;; entities are inserted in the correct order
50
(def mock-db (atom []))
51
​
52
(defn insert*
53
"Simulates inserting records in a db by conjing values onto an
54
atom. ent-tye is `:user`, `:post`, or `:like`, corresponding to the
55
keys in the schema. `spec-gen` is the map generated by clojure.spec"
56
[{:keys [data] :as db} {:keys [ent-type spec-gen]}]
57
(swap! mock-db conj [ent-type spec-gen]))
58
​
59
(defn insert [query]
60
(reset! id-seq 0)
61
(reset! mock-db [])
62
(-> (sg/ent-db-spec-gen {:schema schema} query)
63
(sm/visit-ents-once :inserted-data insert*))
64
;; normally you'd return the expression above, but return nil for
65
;; the example to not produce overwhelming output
66
nil)
67
​
68
;;-------*****--------
69
;; Begin snippets to try in REPL
70
;;-------*****--------
71
​
72
;; Return a map of user entities and their spec-generated data
73
(-> (sg/ent-db-spec-gen {:schema schema} {:user [[3]]})
74
(sm/attr-map :spec-gen))
75
​
76
;; You can specify a username and id
77
(-> (sg/ent-db-spec-gen {:schema schema} {:user [[1 {:spec-gen {:username "Meeghan"
78
:id 100}}]]})
79
(sm/attr-map :spec-gen))
80
​
81
;; Generating a post generates the user the post belongs to, with
82
;; foreign keys correct
83
(-> (sg/ent-db-spec-gen {:schema schema} {:post [[1]]})
84
(sm/attr-map :spec-gen))
85
​
86
;; Generating a like also generates a post and user
87
(-> (sg/ent-db-spec-gen {:schema schema} {:like [[1]]})
88
(sm/attr-map :spec-gen))
89
​
90
​
91
;; The `insert` function shows that records are inserted into the
92
;; simulate "database" (`mock-db`) in correct dependency order:
93
(insert {:like [[1]]})
94
@mock-db
Copied!
Last modified 2yr ago