-
Notifications
You must be signed in to change notification settings - Fork 49
/
test.clj
165 lines (144 loc) · 6.94 KB
/
test.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
(ns toucan.util.test
"Utility functions for writing tests with Toucan models."
(:require [toucan.db :as db]))
;;; TEMP OBJECTS
;;; ==================================================================================================================
;; For your convenience Toucan makes testing easy with *Temporary Objects*. A temporary object is created and made
;; available to some body of code, and then wiped from that database via a `finally` statement (i.e., whether the body
;; completes successfully or not). This makes it easy to write tests that do not change your test database when they
;; are ran.
;;
;; Here's an example of a unit test using a temporary object created via `with-temp`:
;;
;; ;; Make sure newly created users aren't admins
;; (expect false
;; (with-temp User [user {:first-name "Cam", :last-name "Saul"}]
;; (is-admin? user)))
;;
;; In this example, a new instance of `User` is created (via the normal `insert!` pathway), and bound to `user`; the
;; body of `with-temp` (the `test-something` fncall) is executed. Immediately after, the `user` is removed from the
;; Database, but the entire statement returns the results of the body (hopefully `false`).
;;
;; Often a Model will require that many fields be `NOT NULL`, and specifying all of them in every test can get
;; tedious. In the example above, we don't care about the `:first-name` or `:last-name` of the user. We can provide
;; default values for temporary objects by implementing the `WithTempDefaults` protocol:
;;
;; (defn- random-name
;; "Generate a random name of 10 uppercase characters"
;; []
;; (apply str (map char (repeatedly 10 #(rand-nth (range (int \A) (inc (int \Z))))))))
;;
;; (extend-protocol WithTempDefaults
;; (class User)
;; (with-temp-defaults [_] {:first-name (random-name), :last-name (random-name)}))
;;
;; Now whenever we use `with-temp` to create a temporary `User`, a random `:first-name` and `:last-name` will be
;; provided.
;;
;; (with-temp User [user]
;; user)
;; ;; -> {:first-name "RIQGVIDTZN", :last-name "GMYROFEZYO", ...}
;;
;; You can still override any of the defaults, however:
;;
;; (with-temp User [user {:first-name "Cam"}]
;; user)
;; ;; -> {:first-name "Cam", :last-name "OVTAAJBVOF"}
;;
;; Finally, Toucan provides a couple more advanced versions of `with-temp`. The first, `with-temp*`, can be used to
;; create multiple objects at once:
;;
;; (with-temp* [User [user]
;; Conversation [convo {:user_id (:id user)}]]
;; ...)
;;
;; Each successive object can reference the temp object before it; the form is equivalent to writing multiple
;; `with-temp` forms.
;;
;; The last helper macro is available if you use the `expectations` unit test framework:
;;
;; ;; Make sure our get-id function works on users
;; (expect-with-temp [User [user {:first-name "Cam"}]]
;; (:id user)
;; (get-id user))
;;
;; This macro makes the temporary object available to both the "expected" and "actual" parts of the test. (PRs for
;; similar macros for other unit test frameworks are welcome!)
(defprotocol WithTempDefaults
"Protocol defining the `with-temp-defaults` method, which provides default values for new temporary objects."
(with-temp-defaults ^clojure.lang.IPersistentMap [this]
"Return a map of default values that should be used when creating a new temporary object of this model.
;; Use a random first and last name for new temporary Users unless otherwise specified
(extend-protocol WithTempDefaults
(class User)
(with-temp-defaults [_] {:first-name (random-name), :last-name (random-name)}))"))
;; default impl
(extend Object
WithTempDefaults
{:with-temp-defaults (constantly {})})
(defn do-with-temp
"Internal implementation of `with-temp` (don't call this directly)."
[model attributes f]
(let [temp-object (db/insert! model (merge (when (satisfies? WithTempDefaults model)
(with-temp-defaults model))
attributes))]
(try
(f temp-object)
(finally
(db/delete! model :id (:id temp-object))))))
(defmacro with-temp
"Create a temporary instance of ENTITY bound to BINDING-FORM, execute BODY,
then deletes it via `delete!`.
Our unit tests rely a heavily on the test data and make some assumptions about the
DB staying in the same *clean* state. This allows us to write very concise tests.
Generally this means tests should \"clean up after themselves\" and leave things the
way they found them.
`with-temp` should be preferrable going forward over creating random objects *without*
deleting them afterward.
(with-temp EmailReport [report {:creator_id (user->id :rasta)
:name (random-name)}]
...)"
[model [binding-form & [options-map]] & body]
`(do-with-temp ~model ~options-map (fn [~binding-form]
~@body)))
(defmacro with-temp*
"Like `with-temp` but establishes multiple temporary objects at the same time.
(with-temp* [Database [{database-id :id}]
Table [table {:db_id database-id}]]
...)"
[model-bindings & body]
(loop [[pair & more] (reverse (partition 2 model-bindings)), body `(do ~@body)]
(let [body `(with-temp ~@pair
~body)]
(if (seq more)
(recur more body)
body))))
;;; EXPECTATIONS HELPER MACROS
;;; ==================================================================================================================
(defn- has-expectations-dependency? []
(try (require 'expectations)
true
(catch Throwable _
false)))
(when (has-expectations-dependency?)
(defmacro expect-with-temp
"Combines `expect` with a `with-temp*` form. The temporary objects established by `with-temp*` are available
to both EXPECTED and ACTUAL.
(expect-with-temp [Database [{database-id :id}]]
database-id
(get-most-recent-database-id))"
{:style/indent 1}
[with-temp*-form expected actual]
;; use `gensym` instead of auto gensym here so we can be sure it's a unique symbol every time. Otherwise since
;; expectations hashes its body to generate function names it will treat every usage of `expect-with-temp` as
;; the same test and only a single one will end up being ran
(let [with-temp-form (gensym "with-temp-")]
`(let [~with-temp-form (delay (with-temp* ~with-temp*-form
[~expected ~actual]))]
(expectations/expect
;; if dereferencing with-temp-form throws an exception then expect Exception <-> Exception will pass;
;; we don't want that, so make sure the expected is nil
(try
(first @~with-temp-form)
(catch Throwable ~'_))
(second @~with-temp-form))))))