こんにちは。@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
を作りました。
# 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 を利用して、Tasks::DebtManager.clear_all
のロジックが意図した通り振る舞うかテストコードを書いて確認していきます。
テストコード
# 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 "2人から借金中。1人目は送金成功、2人目で残高不足になる" 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_2人から借金中。1人目は送金成功、2人目で残高不足になる
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編もまたいつか。