Clojure テストでモックを使う

![advent_calendar_2024.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/f4cd8ce0-6352-a1d8-4cae-f39dd44f4def.png)

[*Advent Calendar 2024 Day 11*](https://qiita.com/advent-calendar/2024/clojure)

今日は`Clojure`でのテスト時にモックを使う方法について書こうと思います

題材として、Todoの一覧を取得する部分をmockし、取得後の処理のみ部分のテストを書いてみます

```clojure
(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`をモックします

```clojure
(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) ["朝食を食べる" "仕事に行く"]))))))
```

```bash
$ lein test

lein test todo.core-test

Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
```

無事mock化できました

## 2. ライブラリを使う

今回は `mockfn` を使ってみようと思います

https://github.com/nubank/mockfn

`mockfn`は、`providing`と`verifying`という関数があり、これが便利です

#### `providing`

```clojure
(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`の関数をモックしてスタブレスポンスを返しています。

```bash
$ lein test

lein test todo.core-test

Ran 1 tests containing 2 assertions.
0 failures, 0 errors.
```

無事モックできたようでテストが通りました。

ただこれだと`http/get-todo`を本当に使われたのか不明ですし、`verify`したくなることがあると思います。
その時のために次の`verifying`があります

#### `verifying`

`verifying`では、モックした関数が指定した回数呼ばれたかどうかをチェックします

```clojure
(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度だけ呼ばれていることもテストすることができました

```bash
$ lein test

lein test todo.core-test

Ran 1 tests containing 2 assertions.
0 failures, 0 errors.
```

試しに片方だけ`2`回呼ばれたことを検証し、失敗してみます

```bash
$ 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`ということで、モックした関数が呼ばれた関数の違いでテストが失敗するようになりました。