今日はClojure
でのテスト時にモックを使う方法について書こうと思います
題材として、Todoの一覧を取得する部分をmockし、取得後の処理のみ部分のテストを書いてみます
(ns todo.core
(:require [todo.http :as http]))
(defn get-todo-titles
[]
(let [{:keys [body]} (http/get-todos)]
(map :title body)))
例えばこのような、todo.http
からget-todos
という関数を呼び出し、タイトルだけを返却するようなget-todo-titles
があるとします。
get-todo-titles
の単体テストを書くにあたり、todo.http/get-todos
をモックしていきます
1. with-redefs
Clojureの標準ライブラリに含まれるwith-redefs
を使うことで、テストのスコープ内でのみ、グローバルな関数を一時的にモックに置き換えることができます。
これを使ってtodo.http/get-todos
をモックします
(t/deftest get-todo-titles-test
(t/testing "Todoのタイトルを取得できる"
(let [todos [{:id 1 :title "朝食を食べる" :completed false}
{:id 2 :title "仕事に行く" :completed false}]]
(with-redefs [http/get-todos (fn [] {:status 200 :body todos})]
(t/is (= (sut/get-todo-titles) ["朝食を食べる" "仕事に行く"]))))))
$ lein test
lein test todo.core-test
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
無事mock化できました
2. ライブラリを使う
今回は mockfn
を使ってみようと思います
mockfn
は、providing
とverifying
という関数があり、これが便利です
providing
(ns todo.core-test
(:require [clojure.test :as t]
[todo.core :as sut]
[todo.http :as http]
[mockfn.macros :as mockfn]))
(t/deftest get-todo-titles-test
(t/testing "特定のTodoのタイトルを取得できる"
(mockfn/providing
[(http/get-todo 1) {:status 200 :body {:id 1 :title "朝食を食べる"}}
(http/get-todo 2) {:status 200 :body {:id 1 :title "仕事に行く"}}]
(t/is (= (sut/get-todo-title 1) "朝食を食べる"))
(t/is (= (sut/get-todo-title 2) "仕事に行く")))))
ここでは、
id | title | completed |
---|---|---|
1 | 朝食を食べる | false |
2 | 仕事に行く | true |
という状態を想定し、http/get-todo
の関数をモックしてスタブレスポンスを返しています。
$ lein test
lein test todo.core-test
Ran 1 tests containing 2 assertions.
0 failures, 0 errors.
無事モックできたようでテストが通りました。
ただこれだとhttp/get-todo
を本当に使われたのか不明ですし、verify
したくなることがあると思います。
その時のために次のverifying
があります
verifying
verifying
では、モックした関数が指定した回数呼ばれたかどうかをチェックします
(ns todo.core-test
(:require [clojure.test :as t]
[todo.core :as sut]
[todo.http :as http]
[mockfn.macros :as mockfn]
[mockfn.matchers :as matchers]))
(t/deftest get-todo-titles-test
(t/testing "特定のTodoのタイトルを取得でき、http/get-todoが呼ばれている"
(mockfn/verifying
[(http/get-todo 1) {:status 200 :body {:id 1 :title "朝食を食べる" :completed false}} (matchers/exactly 1)
(http/get-todo 2) {:status 200 :body {:id 1 :title "仕事に行く" :complated true}} (matchers/exactly 1)]
(t/is (= (sut/get-todo-title 1) "朝食を食べる"))
(t/is (= (sut/get-todo-title 2) "仕事に行く")))))
このようにすることで、先ほどのテストがさらに良くなり、http/get-todo
が1度だけ呼ばれていることもテストすることができました
$ lein test
lein test todo.core-test
Ran 1 tests containing 2 assertions.
0 failures, 0 errors.
試しに片方だけ2
回呼ばれたことを検証し、失敗してみます
$ lein test
lein test todo.core-test
ERROR in (get-todo-titles-test) (mock.cljc:23)
Uncaught exception, not in assertion.
expected: nil
actual: clojure.lang.ExceptionInfo: Expected todo.http$get_todo@26a2f7f9 with arguments [1] exactly 2 times, received 1.
{}
at mockfn.mock$verify.invokeStatic (mock.cljc:23)
mockfn.mock$verify.invoke (mock.cljc:17)
todo.core_test$fn__781$fn__782.invoke (core_test.clj:27)
clojure.core$with_redefs_fn.invokeStatic (core.clj:7582)
...
Ran 1 tests containing 3 assertions.
0 failures, 1 errors.
Tests failed
このように、[1] exactly 2 times
ということで、モックした関数が呼ばれた関数の違いでテストが失敗するようになりました。