a tutorial introduction - eunmin/Midje GitHub Wiki
.lein/profiles.clj
νμΌμ μλμ κ°μ΄ μΆκ°ν©λλ€ :
{:user {:plugins [[lein-midje "3.0.0"]]}}
μμ λλ ν 리λ₯Ό νλ μ€λΉνκ³ μλμ κ°μ΄ νμ΄νν©λλ€ :
% lein new midje quux
Lein-midjeλ μν ν μ€νΈμ μμ€ μ½λκ° ν¬ν¨λ λλ ν 리 ꡬ쑰λ₯Ό μμ±ν©λλ€ :
Midjeλ μμ€ νμΌκ³Ό ν μ€νΈ νμΌμ ν νμΌμ μμ΄ λμ§λ§ μ΄ νν 리μΌμμλ μΌλ°μ μΈ λ°©λ²μ λ°λΌ νμΌμ λΆλ¦¬ν κ²μ λλ€.
project.clj νμΌμλ Midjeλ₯Ό κ°λ° νκ²½μμ μ¬μ©ν μ μλλ‘ μμ‘΄μ±μ ν¬ν¨νκ³ μμ΅λλ€ :
(defproject quux "0.0.1-SNAPSHOT"
:description "Cool new project to do things and stuff"
:dependencies [[org.clojure/clojure "1.4.0"]]
:profiles {:dev {:dependencies [[midje "1.5.1"]]}}) ;; <<==
κ³Όκ±°μ μ΄λ€ μ΄μ λ‘ Midjeλ μ΄μν λ€μμ€νμ΄μ€λ₯Ό κ°μ§λλ€ :
(ns quux.t-core
(:use midje.sweet) ;; <<==
(:require [quux.core :as core]))
μ¬λ¬λΆμ΄ μ±
μ μ°κ³ conj
λ₯Ό μ€λͺ
νκΈ°λ₯Ό μνλ€κ³ κ°μ ν©μλ€. μ¬λ¬λΆμ "conj appends its element to the end of a vector but to the front of a list or lazy sequence" λΌκ³ μμ±ν μ§λ λͺ¨λ¦
λλ€. κ·Έλ¦¬κ³ μλμ κ°μ΄ 3κ°μ μμ λ₯Ό μμ±ν κ²μ
λλ€ :
(conj [1 2] 3) ;; [1 2 3]
(conj '(1 2) 3) ;; (3 1 2)
(conj (map inc [0 1]) 3) ;; (3 1 2)
λλ μλμ κ°μ΄ μμ±ν μ λ μμ΅λλ€ (the style of my own Functional Programming for the Object-Oriented Programmer):
user=> (conj [1 2] 3)
[1 2 3]
user=> (conj '(1 2) 3)
(3 1 2)
user=> (conj (map inc [0 1]) 3)
(3 1 2)
λλ μ΄λ°μμΌλ‘ μμ± ν μ λ μμ§μ (the style of The Joy of Clojure):
(conj [1 2] 3)
;=> [1 2 3]
(conj '(1 2) 3)
;=> (3 1 2)
(conj (map inc [0 1]) 3)
;=> (3 1 2)
μμ (see also)μ μλ νμ΄νλ μΉμν©λλ€. κ·Έλμ μ¬κΈ°μ νμ΄νλ₯Ό μ°λ κ²μ μ ν©ν©λλ€. κ·Έ μ΄μ λ μΌμͺ½μμ μ€λ₯Έμͺ½μΌλ‘ λμκ°λ μΈμ΄λ₯Ό μ¬μ©νλ μ¬λλ€μ λκ°μ λ°©λ²μΌλ‘ μκ°ν μ μκΈ° λλ¬Έμ
λλ€. (μμ΄λ‘λ λ¬λ ₯μ 보μΈμ.) κ·Έλμ conj
κ΅¬λ¬Έμ΄ μΌμ‘±μ μλ κ²μ μ§κ΄μ μ
λλ€ : λ¨Όμ νκ°λ νμ κ²°κ³Όκ° λμ΄. λ νμ΄νλ μκ° νλ¦μ΄λ ν₯λ―Έλ‘μ΄ μ΄λ€ μΌμ κ°λ¦¬ν¬λ μ¬μ©νλ κ²κ³Ό κ΄λ ¨λμ΄ μκΈ° λλ¬Έμ μμ°μ΅λλ€. κ·Έλ¦¬κ³ μ μ΄λ νμ΄νλ λ κ°μ ννλ₯Ό μκ°μ μΌλ‘ λΆλ¦¬ν΄μ€ μ μμ΅λλ€.
Midje checkablesμ μΌλ°μ μΌλ‘ μλ μμ μ μλ μμ λ₯Ό λ°λ¦ λλ€ :
;; the following pseudo-expressions are checkables
(conj [1 2] 3) => [1 2 3]
(conj '(1 2) 3) => '(3 1 2)
(conj (map inc [0 1]) 3) => '(3 1 2)
μ¬κΈ°μ λ§μ μ¬λλ€μ Lisp κ°μ§ μμ μ΄μν λ¬Έλ²μ λ³Όμ μλλ° ν΄λ‘μ λ λμμΌλ‘ λ€λ₯Έ λ¬Έλ²λ€μ μ 곡 νλ€ :
(expect (conj [1 2] 3) => [1 2 3])
(expect (conj '(1 2) 3) => '(3 1 2))
(expect (conj (map inc [0 1]) 3) => '(3 1 2))
(νμ΄νλ μλ―Έμ μΌλ‘ μ μ ν©λλ€: λ€λ₯Έ μ’ λ₯μ νμ΄νλ μμ΅λλ€.)
νμ§λ§ expect
λ κ±°μ μ¬μ©λμ§ μκΈ° λλ¬Έμ Midje μ¬μ©μλ€μ μ΄μν΄ λ³΄μΌκ²μ
λλ€.
Checkablesλ λ°λ‘ 체ν¬λ₯Ό μν΄ μ¬μ©ν μ μμ΅λλ€. Midjeμ μλ κ°μ₯ μμ λ¨μμ checkableμ factμ
λλ€. μ΄ μ΄λ¦μ μ°λ¦¬κ° μ§μ§λ‘ μνλ κ² μ΄ λ¬΄μμΈκ°μ λν νλμ
λλ€.(: κ°λ ₯ν μνμ μ¬μ€μ λν μμ²μ λ§λ€κΈ° μν) μ°λ¦¬λ "λͺ¨λ λ¬Έμμ΄ sμ λͺ¨λ μ κ·μ reλ (str/split s re)
λ ... νλ€.λ₯Ό λ§μ‘±νλ€"μ κ°μ κ°ν λ¬Έλ²μ λ§λ€κΈ°λ₯Ό μν©λλ€.
κ·Έλ¦¬κ³ μ¬μ€ μ°λ¦¬λ μꡬ μ¬ν κ°μ κ²μ λ§λ€ μ μμ΅λλ€. νμ§λ§ μ°λ¦¬λ μνμλ€μ΄ νλ κ²μ²λΌ μ¦λͺ ν μ μμ΅λλ€. μ°λ¦¬κ° μΌλ°μ μΈ νλ‘κ·Έλλ°μμ ν μ μλ μ΅μ μ λ°©λ²μ μꡬ μ¬νμ΄ μ±κ³΅μΌλ‘ νμΈ λ μ λλ‘ μμ μμ λκΉμ§ μΆ©λΆν μμ λ₯Ό λ§μ΄ μ 곡νκ³ κ·Έκ²μ νμΈνλ μΌμ λλ€. μλ μμ λ₯Ό 보μΈμ :
(fact "`split` splits strings on regular expressions and returns a vector"
(str/split "a/b/c" #"/") => ["a" "b" "c"]
(str/split "" #"irrelevant") => [""]
(str/split "no regexp matches" #"a+\s+[ab]") => ["no regexp matches"])
It's Midje's job to check the checkables (to try to confirm the examples) and report helpfully when they fail.
Let's be realistic
I personally try to think of checkables as examples of more general facts. Other people like to think of them as the facts themselves. Those people tend to use facts
instead of fact
:
(facts "about `split`"
(str/split "a/b/c" #"/") => ["a" "b" "c"]
(str/split "" #"irrelvant") => [""]
(str/split "no regexp matches" #"a+\s+[ab]") => ["no regexp matches"])
Other people think all this talk about facts is nonsense and treat fact
as nothing more than a way of grouping checkables and nested facts, similar to context
in Ruby test frameworks. Midje is agnostic about how you think about its names. If you want to treat "fact" as a misspelling of "test", go ahead.
μ¬λ¬λΆμ μ μμ μΌλ‘ λμνλ clojure.string/split
λ₯Ό ν
μ€νΈνκ³ μΆμ§λ μμκ²λλ€. κ·Έλμ λ²κ·Έλ₯Ό κ°μ§κ³ μλ κ°λ¨ν ν¨μλ₯Ό λ΄
μλ€.
μλ리μ€λ λ€μκ³Ό κ°μ΅λλ€ : ν΄λ‘μ νλ‘κ·Έλλ¨Έκ° μλ€. κ·Έλ λ μνμ€μ 첫λ²μ§Έ νλͺ©μ 리ν΄νλ ν¨μλ₯Ό μμ±νλ €κ³ νλ€. λ§μ½ μνμ€κ° λΉμ΄μλ€λ©΄ κΈ°λ³Έκ°μ 리ν΄νκ² λ§λλ €κ³ ν©λλ€.
(defn first-element [sequence default]
(if (nil? sequence)
default
(first sequence)))
ν΄λ‘μ λ Lisp μ²λΌ nil
κ³Ό ()
λ₯Ό κ°μ΄ μ·¨κΈνμ§ μκΈ° λλ¬Έμ μ΄ ν¨μμλ λ²κ·Έκ° μμ΅λλ€. κ·Έλμ κ·Έλ
λ ν΄λ‘μ νλ‘κ·Έλλ¨Έλ€μ΄ empty?
λ₯Ό μ¬μ©νλ κ² μ²λΌ κ·Έκ±Έ μ¬μ©νκ² λμμ΅λλ€.
νμ§λ§ κ·Έλ λ μ‘°μ¬μ€λ¬μ΄ νλ‘κ·Έλλ¨Έμ΄κΈ° λλ¬Έμ κ·Έλ λ μ μμ μΈ κ²½μ° λΏλ§μλλΌ λ€μν μ’ λ₯μ μνμ€κ° λΉ κ²½μ°μ λλΉνκΈ°λ‘ νμμ΅λλ€. :
(facts "about `first-element`"
(fact "it normally returns the first element"
(first-element [1 2 3] :default) => 1
(first-element '(1 2 3) :default) => 1)
;; I'm a little unsure how Clojure types map onto the Lisp I'm used to.
(fact "default value is returned for empty sequences"
(first-element [] :default) => :default
(first-element '() :default) => :default
(first-element nil :default) => :default
(first-element (filter even? [1 3 5]) :default) => :default))
λ κ°μ μλ‘ λ€λ₯Έ checkableλ₯Ό μν΄μ nested factsλ₯Ό μ¬μ©νκ²μ 보μΈμ. μ‘°μ¬μ€λ½κ² ν λλΆμ λͺκ°μ λ²κ·Έλ₯Ό 체ν¬ν μ μμμ΅λλ€ :
(Midjeκ° μ¬μ©νλ ν°λ―Έλ μμμ 보μ¬μ£ΌκΈ° μν΄μ μ€ν¬λ¦° μ·μ μ¬μ©νμ΅λλ€. μνμ§ μμΌλ©΄ colorizingμ λ μ μμ΅λλ€.)
λ²κ·Έλ₯Ό μμ νκΈ° μ μ lein midje
λ₯Ό μνν λ JVM, Clojure, Midjeλ₯Ό λ‘λνκΈ° λλ¬Έμ μκΈ°λ λλ¦Ό νμμ νΌν μ μλ λ°©λ²μ μλ €λλ¦¬κ² μ΅λλ€.
λ§μ ν
μ€ν
ν΄λ€μ autotestλ₯Ό μ 곡 ν©λλ€. autotest ν΄μ λͺ¨λ κ²μ 체ν¬ν μ μλ νλμ ν
μ€νΈλ₯Ό κ°μ§κ³ μμν©λλ€. νμ§λ§ μ’
λ£λμ§ μκ³ ν
μ€νΈ λλ ν 리μ μμ€μ λ³κ²½μ κ°μ§ν©λλ€. λ³κ²½μ΄ κ°μ§λλ©΄, λ³κ²½ λ νμΌκ³Ό κ·Έ νμΌμ μμ‘΄νκ³ μλ λ€λ₯Έ νμΌλ€μ μ§κ°μ μ μΌλ‘ 리λ‘λ ν©λλ€. (Midjeμ κ²½μ° μμ‘΄μ±μ κ° νμΌμ μλ ns
ꡬ문μ νμλμ΄ μμ΅λλ€.)
Lein-midjeμ autotestingμ μ 컀맨λλΌμΈμμ μμν μ μμ΅λλ€ :
690 $ lein midje :autotest
νμ§λ§ repl ν΄μ μ¬μ©ν λλ λ νΈλ¦¬ν λ°©λ²μ΄ μμ΅λλ€.
μ΄λ κ² νλ©΄ λ©λλ€ :
I'll let you explore Midje's in-repl documentation on your own. For now, type the following:
Notice that you still have the repl prompt, so you can combine autotesting with interactive repl development. For example, you can double-check results:
Go ahead and fix the source. Here's what happened as I did, typo and all.
Midje doesn't use =
to compare the left-hand to right-hand sides, but rather its own extended equality. There are various small β and two large β differences between equality and extended equality.
The first of the large differences is the handling of regular expressions. Equality is a pretty useless concept for them, since regular expressions aren't even equal to non-identical copies:
user=> (= #"a.b" #"a.b")
false
In a Midje checkable, a regular expression on the right-hand side causes a check for a partial match:
user=> (fact "O wad some pow'r" => #"wad\s+some")
true
That is, the checking is done with re-find
.
The larger of the two large differences is the handling of functions on the right-hand side. Under extended equality, the actual result isn't compared to the function. Instead it is passed to the function and the result is checked for "truthiness" (that is, any value other than nil
or false
). Here's a simple example:
user=> (fact (+ 1 2) => even?)
FAIL at (NO_SOURCE_PATH:1)
Actual result did not agree with the checking function.
Actual result: 3
Checking function: even?
false
Midje comes prepackaged with useful functions, called checkers, for your checkables. The two most used are probably truthy
and falsey
. Consider this expression:
(some even? [1 5])
The result of that expression is nil
, so we could write this fact:
(fact
(some even? [1 5]) => nil)
That works, but it does a poor job of expressing intent. Most often, we don't care specifically that the result is nil
β we care that the result counts as false. So this is better:
(fact
(some even? [1 5]) => falsey)
Note: you can also negate the arrow to force a check for inequality:
(fact
(some even? [1 5]) =not=> truthy)
One disadvantage of truthy
and falsey
is that checkables using them often do not read clearly because they don't match the way we normally speak or write. Consider [1 3 5 8]
. Most people would be more likely to say "The array has an [or "some"] even element" than "A search for some even element would yield a truthy value." We can use extended equality to match the way people speak:
(fact
[1 3 5 8] => #(some even? %) ; or...
[1 3 5 8] => (partial some even?))
Unless you're a huge fan of higher-order functions and can grasp their meaning immediately, that looks kind of awkward. Midje provides a checker that looks nicer:
(fact
[1 3 5 8] => (contains even?))
This works because contains
interprets its arguments using extended equality (so that functions are given actual results as arguments). However, it and other collection checkers are probably most often used with non-function values. Here are two examples. I've made them fail so you can see what failure output looks like:
user=> (fact {:a 1, :b 2} => (just {:a 1, :CCC 333}))
FAIL at (NO_SOURCE_PATH:1)
Actual result did not agree with the checking function.
Actual result: {:a 1, :b 2}
Checking function: (just {:CCC 333, :a 1})
The checker said this about the reason:
Best match found: {:a 1}
false
user=> (fact [1 2 3 4 5] => (contains [1 2 4]))
FAIL at (NO_SOURCE_PATH:1)
Actual result did not agree with the checking function.
Actual result: [1 2 3 4 5]
Checking function: (contains [1 2 4])
The checker said this about the reason:
Best match found: [1 2]
false
The second example fails because contains
by default looks for a contiguous subsequence. That can be overridden:
user=> (fact [1 2 3 4 5] => (contains [1 2 4] :gaps-ok))
true
Test-driven design can be done either bottom up (in a way reminiscent of traditional Lisp repl-driven development) or top down (as described in Growing Object-Oriented Software, Guided by Tests, one of the strong early inspirations for Midje). Since there are various paths through this user documentation, I'll point you to this introduction if you're interested in learning about how Midje views the top-down approach in a functional language.
With Midje, I've aimed to support bottom-up design, top-down design, and (most importantly) a smooth alternation between the two. I've also aimed to combine the ease of repl-based development with the long-term value of putting tests in files. To judge how well I've met my goals, you'll have to put Midje to use.