-
Notifications
You must be signed in to change notification settings - Fork 0
/
behajour.clj
455 lines (396 loc) · 15.6 KB
/
behajour.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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
(ns behajour
(:use clojure.test)
(:use clojure.contrib.str-utils))
(import 'java.io.StringWriter)
(import 'java.util.HashMap)
(defstruct step :stage :keywords :implementation)
(with-test
(defn are-keywords-same? [first-keywords second-keywords]
(if (or (not (= (count first-keywords) (count second-keywords)))
(some false? (map #(or (and (fn? %1) (fn? %2)) (= %1 %2)) first-keywords second-keywords)))
false
true))
(is (true? (are-keywords-same? ["a" "b" "c"] ["a" "b" "c"])))
(is (true? (are-keywords-same? ["a" #(str %) "c"] ["a" #(num %) "c"])))
(is (false? (are-keywords-same? ["a" #(str %) "c" "d"] ["a" #(num %) "c"])))
(is (false? (are-keywords-same? ["a" #(str %) "c" "d"] ["a" "c" #(num %) "d"])))
(is (false? (are-keywords-same? ["a" "b" "c" "d"] ["a" "b" "c"]))))
(with-test
(defn check-first-step-has-different-keywords-to-the-rest [new-list]
(if (< 1 (count new-list))
(let [new-step (first new-list)
old-steps (rest new-list)]
(not (some #(are-keywords-same? (new-step :keywords) (:keywords %)) old-steps)))
true))
(is (true? (check-first-step-has-different-keywords-to-the-rest (list (struct step :given ["a" "b"]),
(struct step :given ["a" "c"]))))))
(def *steps* (ref []))
(def test-fn-map (new HashMap))
(with-test
(defn- map-elements-to-strings [[ & elements]]
(map #(str %) elements))
(is (= ["a" "b" "c"] (map-elements-to-strings ['a 'b 'c]))))
(with-test
(defn- evalable? [s]
(if (= 'clojure.core/unquote (first s))
(read-string (apply str (rest s)))
nil))
(is (nil? (evalable? "lkjl")))
(is (= 'lkj (evalable? '~lkj)))
(is (= 1 (evalable? '~1)))
(is (= nil (evalable? "~1")))
(is false? (if (evalable? "lkjk") true false))
(is true? (if (evalable? "~lkjk") true false)))
(with-test
(defn- map-elements-to-fns-or-strings [[ & elements]]
(map #(try (let [evalable-element (evalable? %)]
(if evalable-element
(let [result (eval evalable-element)]
(if (fn? result)
evalable-element
(str %)))
(str %)))
(catch java.lang.Throwable e (str %)))
elements))
(is (= ["a" "b" "c"] (map-elements-to-fns-or-strings ['a 'b 'c])))
(is (fn? (eval (first (map-elements-to-fns-or-strings ['~(fn [] 1)])))))
(binding [test-fn-map #(num %)]
(is (= ["a" "b" 'test-fn-map] (map-elements-to-fns-or-strings ['a 'b '~test-fn-map])))
(is (= ["a" "b" "test-fn-map"] (map-elements-to-fns-or-strings ['a 'b 'test-fn-map])))))
(with-test
(defn define-step [stage keywords func]
(dosync
(let [idx (first
(first
(filter (fn [[_ stp]] (and (= stage (:stage stp))
(are-keywords-same? (:keywords stp) keywords)))
(map vector (iterate inc 0) @*steps*))))
new-step (struct step stage keywords func)]
(if (nil? idx)
(alter *steps* conj new-step)
(alter *steps* assoc idx new-step))
idx)))
(binding [*steps* (ref [])]
(let [impl (fn [] 1)]
(define-step :given ["a" "b" "c"] impl)
(is (= @*steps* [{:stage :given :keywords ["a" "b" "c"] :implementation impl}]))))
(binding [*steps* (ref [{:stage :given :keywords ["a" "b" "c"] :implementation (fn [] 0)}])]
(let [impl (fn [] 1)]
(define-step :given ["a" "b" "c"] impl)
(is (= @*steps* [{:stage :given :keywords ["a" "b" "c"] :implementation impl}])))))
(with-test
(defn- make-keyword [keywd]
(let [k (. (str keywd) toLowerCase)]
(if (some #(= k %) ["given" "when" "then"])
(keyword k)
(throw (new IllegalArgumentException (str "unexpected keyword : " keyword))))))
(is (thrown? IllegalArgumentException (make-keyword 'asdf)))
(is (= :given (make-keyword 'Given)))
(is (= :when (make-keyword "when")))
(is (= :then (make-keyword 'THEN))))
(defmacro defstep "Use this to create the steps that implement the scenarios"
[[stage & keywords] args & body]
`(define-step ~(make-keyword stage) ~(vec (map-elements-to-fns-or-strings keywords))
(fn ~args ~@body)))
(deftest test-defstep
(is (= `(define-step :given ["a" "thing"]
(fn [] (println "fish")))
(macroexpand-1
'(defstep
[Given a thing]
[]
(clojure.core/println "fish")))))
(is (vector? (nth (macroexpand-1 '(defstep [given a thing] [] (println "fish"))) 2)))
(binding [test-fn-map #(num %)]
(is (= `(define-step :given ["a" test-fn-map]
(fn [] (println "frog")))
(macroexpand-1
(list 'defstep
['given 'a '~behajour/test-fn-map]
[]
'(clojure.core/println "frog")))))))
(with-test
(defn- match-step-keywords? [scenario-keywords step-keywords]
(let [scenario-keyword (first scenario-keywords)
step-keyword (first step-keywords)
compare-as-strings (fn [sc st] (and (string? st) (. st equalsIgnoreCase sc)))]
(cond
(and (= 0 (count scenario-keywords) (count step-keywords)))
true
(and (fn? step-keyword) (= 1 (count step-keywords))
(= 0 (count scenario-keywords)))
true
(compare-as-strings scenario-keyword step-keyword)
(recur (rest scenario-keywords) (rest step-keywords))
(fn? step-keyword)
(if (compare-as-strings (second scenario-keywords) (second step-keywords))
(recur (rest scenario-keywords) (rest step-keywords))
(recur (rest scenario-keywords) step-keywords))
:else false)))
(is (= false (match-step-keywords? ["a"] ["a" "b"])))
(is (= true (match-step-keywords? ["a" "b" "c"] ["a" "b" "c"])))
(is (= true (match-step-keywords? ["aaa" "bbb" "ccc"] ["aaa" "bbb" "ccc"])))
(is (= false (match-step-keywords? ["a" "b" "Q"] ["a" "b" "c"])))
(is (= true (match-step-keywords? ["a" "b" "c" "d" "e"] ["a" (fn [b c d] 1) "e"])))
(is (= false (match-step-keywords? ["a" "b" "c" "d" "e"] ["a" (fn [b c] 1) "d"])))
(is (= true (match-step-keywords? ["a" "b" "c" "d" "e"] ["a" (fn [b c d e] 1)])))
(is (= true (match-step-keywords? ["a" "b" "c" "d" "e"] ["a" #(%) "c" #(%) "e"])))
(is (= true (match-step-keywords? ["a" "b" "c" "d"] ["a" #(%) "c" #(%)]))))
(with-test
(defn- get-matching-steps [stage keywords]
(filter (fn [step]
(and (= stage (step :stage))
(match-step-keywords? keywords (step :keywords))))
@*steps*))
(binding [*steps* (ref [{:stage :given :keywords ["a" "b" "c"] :implementation (fn [] 1)}
{:stage :when :keywords ["a" "b" "c"] :implementation (fn [] 2)}
{:stage :then :keywords ["a" "b" "c"] :implementation (fn [] 3)}])]
(is (= [(first @*steps*)]
(get-matching-steps :given ["a" "b" "c"])))))
(with-test
(defn- match-steps? [stage keywords]
(let [matching-steps (get-matching-steps stage keywords)]
(if (= 1 (count matching-steps))
(first matching-steps)
false)))
(binding [*steps* (ref [{:stage :given :keywords ["a" "b" "c"] :implementation (fn [] 1)}
{:stage :when :keywords ["a" "b" "c"] :implementation (fn [] 2)}
{:stage :then :keywords ["a" "b" "c"] :implementation (fn [] 3)}])]
(is (= (first @*steps*) (match-steps? :given ["a" "b" "c"])))
(is (= (second @*steps*) (match-steps? :when ["a" "b" "c"])))
(is (= (nth @*steps* 2) (match-steps? :then ["a" "b" "c"])))))
(defn- tokenize-scenario
([test-clauses]
(tokenize-scenario nil [] test-clauses))
([stage accum test-clauses]
(if (= 0 (count test-clauses))
accum
(let [token (. (str (first test-clauses)) toLowerCase)]
(if stage
(if (. "and" equalsIgnoreCase token)
(recur stage (conj accum stage) (vec (rest test-clauses)))
(if (or (. "then" equals token) (. "when" equals token))
(recur (make-keyword token) (conj accum (keyword (. token toLowerCase))) (vec (rest test-clauses)))
(recur stage (conj accum token) (vec (rest test-clauses)))))
(if (not (= token "given"))
(throw (new IllegalArgumentException (str "missing given - found " token)))
(recur :given [:given] (vec (rest test-clauses)))))))))
(defn- group-tokens
([tokens]
(group-tokens [] tokens))
([accum tokens]
(if (= 0 (count tokens))
accum
(let [token (first tokens)
last-accum-element-index (- (count accum) 1)]
(if (keyword? token)
(recur (conj accum [token]) (rest tokens))
(recur (assoc accum last-accum-element-index
(conj (get accum last-accum-element-index) token))
(rest tokens)))))))
(with-test
(defn parse-scenario [ & test-clauses]
(group-tokens (tokenize-scenario test-clauses)))
(is (= [[:given "a"] [:when "b"] [:then "c"]]
(parse-scenario "Given" "a" "When" "b" "Then" "c"))))
(with-test
(defn capitalise [word]
(str (. (str (first word)) toUpperCase) (. (subs word 1) toLowerCase)))
(is (= "Upper" (capitalise "upper"))))
(defn- print-step [stage test-clauses]
(print (str (capitalise (name stage)) " " (str-join " " test-clauses))))
(with-test
(defn- get-test-fn-args
([scenario-keywords step-keywords]
(get-test-fn-args [] scenario-keywords step-keywords []))
([accum scenario-keywords step-keywords args-to-be-converted]
(if (or (= 0 (count scenario-keywords))
(= 0 (count step-keywords)))
accum
(if (= (first step-keywords) (first scenario-keywords))
(recur accum (rest scenario-keywords) (rest step-keywords) [])
(if (= (second step-keywords)
(second scenario-keywords))
(recur (conj accum (apply (first step-keywords) (conj args-to-be-converted (first scenario-keywords)))) (rest scenario-keywords) (rest step-keywords) [])
(recur accum (rest scenario-keywords) step-keywords (conj args-to-be-converted (first scenario-keywords))))))))
(is (= [] (get-test-fn-args ["a" "b" "c"] ["a" "b" "c"])))
(is (= [["d" "c" "b"]] (get-test-fn-args ["a" "b" "c" "d" "e"] ["a" (fn [b c d] [d c b]) "e"])))
(is (= ["b" "d"] (get-test-fn-args ["a" "b" "c" "d" "e"] ["a" #(str %) "c" #(str %) "e"])))
(is (= ["b"] (get-test-fn-args ["a" "b" "c"] ["a" #(str %) "c"]))))
(defn execute-scenario
([title tests]
(binding [*out* (new StringWriter)]
(println (str "\n Scenario : " title " (PENDING)\n"))
(if (execute-scenario title tests false)
(let [pending-test-info (str *out*)]
(with-test-out
(println pending-test-info))))))
([_ tests test-known-pending]
(if (< 0 (count tests))
(let [test-line (first tests)
[stage & test-clauses] test-line
step (match-steps? stage test-clauses)]
(print-step stage test-clauses)
(if step
(do (if (not test-known-pending)
(apply (step :implementation) (get-test-fn-args test-clauses (step :keywords))))
(println " "))
(println " (PENDING)"))
(if step
(recur 't (rest tests) test-known-pending)
(recur 't (rest tests) true)))
test-known-pending)))
(defmacro scenario "The BDD scenario definition macro"
[title & test-clauses]
(let [test-name (re-gsub #"'" "-" (re-gsub #"[^a-zA-Z ?+*/!_-]" "" (re-gsub #" " "-" (str "behajour-test-" title))))]
`(deftest ~(symbol test-name)
(execute-scenario ~title (parse-scenario ~@(map-elements-to-strings test-clauses))))))
(defn run-behajour-tests
([] (run-tests))
([& namespaces]
(run-tests namespaces)))
(deftest integration-test
(binding [*steps* (ref [])
test-fn-map (new HashMap)]
(try
(defstep
[given a precondition with ~(fn [& args] (str args))]
[data]
(print " -- " data " --"))
(defstep
[then another result is equal to some data]
[]
(print " -- should not be executed --"))
(scenario "the bdd library runs tests in a given, when, then format"
given a precondition with a bit of data in
and another precondition
When an action happens
and another action happens that requires some input
Then a result is true
and another result is equal to some data
and a final test that we received some input)
(behajour-test-the-bdd-library-runs-tests-in-a-given-when-then-format)
(finally (def behajour-test-the-bdd-library-runs-tests-in-a-given-when-then-format nil)))))
(deftest test-multiple-actions
(binding [*steps* (ref [])
test-fn-map (new HashMap)]
(try
(defstep
[given a ~#(str %) number ~#(. Integer parseInt %)]
[k n]
(. test-fn-map put k n))
(defstep
[When the numbers are summed]
[]
(. test-fn-map put "answer" (+ (. test-fn-map get "first")
(. test-fn-map get "second"))))
(defstep
[When the numbers are multiplied]
[]
(. test-fn-map put "answer" (* (. test-fn-map get "first")
(. test-fn-map get "second"))))
(defstep
[Then a result is ~#(. Integer parseInt %)]
[n]
(is (= n (. test-fn-map get "answer"))))
(scenario "the + and * functions"
Given a first number 1
and a second number 2
When the numbers are summed
Then a result is 3
When the numbers are multiplied
Then a result is 2)
(behajour-test-the-+-and-*-functions)
(finally (def behajour-test-the-+-and-*-functions nil)))))
(deftest test-steps-are-displayed-as-pending
(try
(let [test-results (new StringWriter)]
(binding [*steps* (ref ())
*test-out* test-results]
(defstep [given one that does] [& args] (println args))
(scenario "test pending" Given a step that does not exist and one that does)
(behajour-test-test-pending))
(is (. (str test-results) contains "Scenario : test pending (PENDING)
Given a step that does not exist (PENDING)
Given one that does")))
(finally (def behajour-test-test-pending nil))))
(deftest test-strings-group-tokens
(binding [*steps* (ref [])
test-fn-map (new HashMap)]
(try
(defstep
[given a string ~#(do (. test-fn-map put "first conversion string called" "true") %)]
[arg]
(. test-fn-map put :given arg))
(defstep
[when the tokens are counted]
[]
(. test-fn-map put :when "when"))
(defstep
[then there is ~#(do (. test-fn-map put "second conversion string called" "true") %) token]
[arg]
(. test-fn-map put :then arg))
(scenario "strings are one token"
Given a string "with some spaces"
When the tokens are counted
Then there is 1 token)
(behajour-test-strings-are-one-token)
(is (= 3 (count @*steps*)))
(is (= "with some spaces" (. test-fn-map get :given)))
(is (= "when" (. test-fn-map get :when)))
(is (= "1" (. test-fn-map get :then)))
(is (= "true" (. test-fn-map get "first conversion string called")))
(is (= "true" (. test-fn-map get "second conversion string called")))
(finally (def behajour-test-strings-are-one-token nil)))))
(deftest test-many-scenarios
(binding [*steps* (ref [])
test-fn-map (new HashMap)]
(try
(ns behajour-test
(:use behajour))
(defstep
[given a string ~#(do (. test-fn-map put "first conversion string called" "true") %)]
[arg]
(. test-fn-map put :given arg))
(defstep
[when the tokens are counted]
[]
(. test-fn-map put :when "when"))
(defstep
[then there is ~#(do (. test-fn-map put "second conversion string called" "true") %) token]
[arg]
(. test-fn-map put :then arg))
(defstep
[Given a predicate]
[] (print " -- my first step"))
(scenario "first scenario"
Given a string "with some spaces"
When the tokens are counted
Then there is 1 token)
(scenario "second scenario"
Given a string "with some more spaces"
When the tokens are counted
Then there is 1 token)
(scenario "third scenario"
Given a string "with some more spaces"
When the tokens are counted
Then there is 1 token)
(scenario "My first scenario"
Given a predicate
When an action happens
Then a result)
(scenario "the + function"
Given a first number 1
and a second number 2
When the numbers are summed
Then a result is 3)
(defstep
[Then a result]
[] (print " -- my third step"))
(run-behajour-tests)
(finally (def behajour-test-first-scenario nil)
(def behajour-test-second-scenario nil)
(def behajour-test-third-scenario nil)
(def behajour-test-My-first-scenario nil)
(def behajour-test-the-+-function nil)
(in-ns 'behajour)))))