a tutorial introduction - eunmin/Midje GitHub Wiki

.lein/profiles.clj νŒŒμΌμ— μ•„λž˜μ™€ 같이 μΆ”κ°€ν•©λ‹ˆλ‹€ :

{:user {:plugins [[lein-midje "3.0.0"]]}}

μž‘μ—… 디렉토리λ₯Ό ν•˜λ‚˜ μ€€λΉ„ν•˜κ³  μ•„λž˜μ™€ 같이 νƒ€μ΄ν•‘ν•©λ‹ˆλ‹€ :

% lein new midje quux

Lein-midjeλŠ” μƒ˜ν”Œ ν…ŒμŠ€νŠΈμ™€ μ†ŒμŠ€ μ½”λ“œκ°€ ν¬ν•¨λœ 디렉토리 ꡬ쑰λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€ :

Test and source directories

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]))

Checkables

μ—¬λŸ¬λΆ„μ΄ 책을 μ“°κ³  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 μ‚¬μš©μžλ“€μ€ 이상해 λ³΄μΌκ²ƒμž…λ‹ˆλ‹€.

Facts

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.

Checking facts

μ—¬λŸ¬λΆ„μ€ μ •μƒμ μœΌλ‘œ λ™μž‘ν•˜λŠ” 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λ₯Ό μ‚¬μš©ν•œκ²ƒμ„ λ³΄μ„Έμš”. μ‘°μ‹¬μŠ€λŸ½κ²Œ ν•œ 덕뢄에 λͺ‡κ°œμ˜ 버그λ₯Ό 체크할 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€ :

A lein-midje failure

(Midjeκ°€ μ‚¬μš©ν•˜λŠ” 터미널 색상을 보여주기 μœ„ν•΄μ„œ 슀크린 샷을 μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€. μ›ν•˜μ§€ μ•ŠμœΌλ©΄ colorizing을 끌 수 μžˆμŠ΅λ‹ˆλ‹€.)

버그λ₯Ό μˆ˜μ •ν•˜κΈ° 전에 lein midjeλ₯Ό μˆ˜ν–‰ν•  λ•Œ JVM, Clojure, Midjeλ₯Ό λ‘œλ“œν•˜κΈ° λ•Œλ¬Έμ— μƒκΈ°λŠ” 느림 ν˜„μƒμ„ ν”Όν•  수 μžˆλŠ” 방법을 μ•Œλ €λ“œλ¦¬κ² μŠ΅λ‹ˆλ‹€.

Autotest

λ§Žμ€ ν…ŒμŠ€νŒ… νˆ΄λ“€μ€ autotestλ₯Ό 제곡 ν•©λ‹ˆλ‹€. autotest νˆ΄μ€ λͺ¨λ“  것을 체크할 수 μžˆλŠ” ν•˜λ‚˜μ˜ ν…ŒμŠ€νŠΈλ₯Ό κ°€μ§€κ³  μ‹œμž‘ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ μ’…λ£Œλ˜μ§€ μ•Šκ³  ν…ŒμŠ€νŠΈ 디렉토리와 μ†ŒμŠ€μ˜ 변경을 κ°μ§€ν•©λ‹ˆλ‹€. 변경이 κ°μ§€λ˜λ©΄, λ³€κ²½ 된 파일과 κ·Έ 파일의 μ˜μ‘΄ν•˜κ³  μžˆλŠ” λ‹€λ₯Έ νŒŒμΌλ“€μ„ μ§κ°„μ ‘μ μœΌλ‘œ λ¦¬λ‘œλ“œ ν•©λ‹ˆλ‹€. (Midje의 경우 μ˜μ‘΄μ„±μ€ 각 νŒŒμΌμ— μžˆλŠ” nsꡬ문에 ν‘œμ‹œλ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.)

Lein-midje의 autotesting은 은 μ»€λ§¨λ“œλΌμΈμ—μ„œ μ‹œμž‘ν•  수 μžˆμŠ΅λ‹ˆλ‹€ :

690 $ lein midje :autotest

ν•˜μ§€λ§Œ repl νˆ΄μ„ μ‚¬μš©ν• λ•ŒλŠ” 더 νŽΈλ¦¬ν•œ 방법이 μžˆμŠ΅λ‹ˆλ‹€.

Autotesting in the repl

μ΄λ ‡κ²Œ ν•˜λ©΄ λ©λ‹ˆλ‹€ :

Using the repl tools

I'll let you explore Midje's in-repl documentation on your own. For now, type the following:

Autotest in the repl

Notice that you still have the repl prompt, so you can combine autotesting with interactive repl development. For example, you can double-check results:

Using the repl tools

Go ahead and fix the source. Here's what happened as I did, typo and all.

Using the repl tools

Extended equality and checkers

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

Top-down development and the logical structure of programs

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.

In summary

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.

⚠️ **GitHub.com Fallback** ⚠️