Rails minitest 使い方(具体例あり) - Mock編

こんにちは。[@koshi_life](https://twitter.com/koshi_life) です。
2018年末からRubyを書きはじめて、テストの書き方について調べた内容を備忘します。

モデルとか画面描画を挟まないロジックならテスト書きやすいので積極的にテストコードを書きたい派です。
プロダクト、チーム状況にはよって敢えてテスト書かない意思決定をするのも全然OKだと思いますが、

「敢えて書かない」と「書けないから書かない」のは違うので、ちゃんとテストが書ける技術は身につけておきたいものです。

Rails 標準のテストフレームワーク minitest を使ってみました。

## 前提

- Ruby 2.6.0
- Rails 5.2.2

## テスト対象モジュール

ユーザ間でお金の貸し借りを行えるアプリをイメージして、新規でRailsアプリを作ります。(DB設定は割愛)

```
$ rails new hello-rails --skip-active-record
$ rails g model bank_account account_id amount:Integer
```

テスト対象モジュールとして、
ユーザが借りている借金を貸した人全員に全額返却する機能
`Tasks::DebtManager.clear_all` を作りました。

```lib/tasks/debt_manager.rb
# coding: utf-8

# 借金マネージャー
class Tasks::DebtManager

    # 借金を借りている全員に対して返済する。
    def self.clear_all (account_id, payment_api_client)
        #　<API利用> 借金リストを取得します。
        debts = payment_api_client.list_debts(account_id)

        # 借りている1人1人に返却します。
        debts.each do |debt|
            # 送金APIに必要なパラメーターを準備します。
            from = account_id
            to = debt[:account_id]
            amount = debt[:amount]

            Rails.logger.info("===== #{from} さんが #{to} さんに借りているお金(#{amount}円)を精算します。 =====")

            # <API利用> 送金します。
            result = payment_api_client.send_money(from, to, amount)

            # 処理成否に応じて残高（DB)を更新します。
            if result[:result]
                # 成功パターンなら、送金額をDBに反映します。(トランザクション処理は割愛)
                from_bank = BankAccount.find_by(account_id: from)
                to_bank = BankAccount.find_by(account_id: to)
                from_bank.amount -= amount
                to_bank.amount += amount

                # DB内容を更新
                from_bank.save()
                to_bank.save()

                Rails.logger.info("借金を返済しました。")
            else
                # 失敗パターンなら、DBは更新しない。
                Rails.logger.error("残高不足のため借金の返済ができませんでした。")
            end
        end
    end
end
```

このコードのポイントは、引数の本モジュール外の機能 `payment_api_client` を`<API利用>`のコメント箇所で用いている部分です。

`payment_api_client` は　`Tasks::DebtManager.clear_all`　の単体テストでは担保すべき範囲外なので、 [Mock](https://www.rubydoc.info/gems/minitest/4.2.0/MiniTest/Mock#expect-instance_method) を利用して、`Tasks::DebtManager.clear_all`　のロジックが意図した通り振る舞うかテストコードを書いて確認していきます。

## テストコード


```test/tasks/debt_manager_test.rb
# coding: utf-8

require 'minitest/autorun'

# DB内 初期化モジュール, DBは mongoDB　(mongoid) です。
module ModelCleaner
    # Test Data
    BANK_ACCOUNTS = [
        BankAccount.new(account_id: 'acc1', amount: 600),
        BankAccount.new(account_id: 'acc2', amount: 1000),
        BankAccount.new(account_id: 'acc3', amount: 2000),
    ]
    def initialize()
        Rails.logger.info("ModelCleaner.initialize() is called.")
        # Drop Collections
        BankAccount.collection.drop()
        # Insert Data
        BANK_ACCOUNTS.each { |ba| ba.save!()}
    end
    module_function :initialize
end


# テストクラス
class DebtManagerTest < ActiveSupport::TestCase
    # DBを初期化します。
    ModelCleaner.initialize()

    test "２人から借金中。１人目は送金成功、２人目で残高不足になる" do
        # Mock準備 payment_api_client で返却する値を確認ケースに沿って定義します。
        list_debts_res1 = [
            { account_id: 'acc2', amount: 500 },
            { account_id: 'acc3', amount: 1000 },
        ]
        send_money_res1 = { result: true }
        send_money_res2 = { result: false }

        # Mockを定義します。
        payment_api_mock = MiniTest::Mock.new
        payment_api_mock.expect(:list_debts, list_debts_res1, ['acc1'])
        payment_api_mock.expect(:send_money, send_money_res1, ['acc1', 'acc2', 500])
        payment_api_mock.expect(:send_money, send_money_res2, ['acc1', 'acc3', 1000])

        # 検証対象メソッド実行します。
        Tasks::DebtManager.clear_all('acc1', payment_api_mock)

        # Mockに送信されたデータを検証します。
        payment_api_mock.verify

        # 送金後のDB内が期待値通りか検証します。
        ba1 = BankAccount.find_by(account_id: 'acc1')
        assert_equal(100, ba1.amount.to_i)
        ba2 = BankAccount.find_by(account_id: 'acc2')
        assert_equal(1500, ba2.amount.to_i)
        ba3 = BankAccount.find_by(account_id: 'acc3')
        assert_equal(2000, ba3.amount.to_i)
    end

end
```

ポイントは MiniTest::Mock#expect 関数の使い方かと。
公式ドキュメントにあるように、

>MiniTest::Mock#expect(name, retval, args = []) ⇒ Object

第一引数にコールされるメソッド名
第二引数に返却値
第三引数にメソッドコール時に指定する引数
を指定します。

>payment_api_mock.expect(:send_money, send_money_res1, ['acc1', 'acc2', 500])
>payment_api_mock.expect(:send_money, send_money_res2, ['acc1', 'acc3', 1000])

同じメソッドを複数回呼ぶ想定なら、上記のように呼ばれる順番でexpect関数で登録します。


## テスト実行結果

```
$ rails test test/tasks/debt_manager_test.rb 
Running via Spring preloader in process 6760
I, [2019-02-01T20:06:40.389554 #6760]  INFO -- : ModelCleaner.initialize() is called.
Run options: --seed 57956

# Running:

I, [2019-02-01T20:06:40.481603 #6760]  INFO -- : --------------------------------------------------
I, [2019-02-01T20:06:40.481768 #6760]  INFO -- : DebtManagerTest: test_２人から借金中。１人目は送金成功、２人目で残高不足になる
I, [2019-02-01T20:06:40.481856 #6760]  INFO -- : --------------------------------------------------
I, [2019-02-01T20:06:40.485079 #6760]  INFO -- : ===== acc1 さんが acc2 さんに借りているお金(500円)を精算します。 =====
I, [2019-02-01T20:06:40.495268 #6760]  INFO -- : 借金を返済しました。
I, [2019-02-01T20:06:40.495409 #6760]  INFO -- : ===== acc1 さんが acc3 さんに借りているお金(1000円)を精算します。 =====
E, [2019-02-01T20:06:40.495508 #6760] ERROR -- : 残高不足のため借金の返済ができませんでした。
.

Finished in 0.024644s, 40.5778 runs/s, 121.7335 assertions/s.
1 runs, 3 assertions, 0 failures, 0 errors, 0 skips
```

これで、Mock使いになれた気がします。Stub編もまたいつか。

## 参考

- [Rails テスティングガイド](https://railsguides.jp/testing.html)
- [スタブとモックの違い](https://qiita.com/k5trismegistus/items/10ce381d29ab62ca0ea6)
- [minitest で mock や stub を使う](https://blog.willnet.in/entry/2012/12/05/004010)