Rails製のアプリケーションのコントローラーの単体テストを見ていると内部で使用しているクラスやモジュールの観点・パターンまでを網羅したファットな単体テストが書かれているものを見かけます。しかしながらコントローラーの単体テストはあくまでコントローラーの責務について注目して書かれるべきであって、内部で使用しているクラスやモジュールの観点・パターンにまで踏み込む必要はありません。
ではコントローラーの単体テストで内部で使用しているクラス・モジュールがある場合に、どのように単体テストを書けば良いのでしょうか。 自分がよくやるのは、呼び出し時の引数を確認するという方法です。
サンプルコード
以下のようなRails製のアプリケーションに、データ同期用のコントローラーがあるとします。
このコントローラーでは同期用クラスのインスタンスを作成して、適当なメソッドを呼び出します。
controllers/sync_controller.rb
require 'sync_request' class SyncController < ApplicationController def create options = { option_a: true, option_b: false } ins = SyncRequest.new( permitted_params[:table_name], permitted_params[:column_name], permitted_params[:value], options, ) ins.exec render status: :ok, json: {} end def permitted_params params.permit(:table_name, :column_name, :value) end end
lib/sync_request.rb
class SyncRequest def initialize(table_name, column_name, value, options) @table_name = table_name @column_name = column_name @value = value @options = options end def exec # do something... puts 'exec!' end end
ファイルの全体像はgithubにて公開しているので、合わせてご覧ください。
前提条件について
今回、紹介していく引数を確認する単体テストですが以下の前提の元で採用をしています。
- クラス・モジュールの関数呼び出しにおいて副作用がないこと
- 引数によって関数の振る舞いが決定されること
「呼び出し時の引数を確認する」単体テスト
先ほど書いた「呼び出し時の引数を確認する」単体テストについて紹介します。
rspecのallow
とreceive
のブロックを組み合わせることで、特定の関数が呼び出された時の振る舞いをオーバーライドすることが可能なのですが、このブロックの引数には呼び出し時に指定された値が格納されています。
引数の数だけdo |a, b, c|
と書くことができますが、面倒なのでdo |*args|
と受け取ると楽です。指定された値が配列で受け取れるので、ここにexpect
を書けば、値が想定通りに指定されているかを確認する単体テストが出来上がります。
require 'rails_helper' require 'sync_request' RSpec.describe "SyncController", type: :request do describe 'POST: /sync' do let(:request_params) do { table_name: 'users', column_name: 'name', value: '有馬かな', } end context 'SyncRequest' do it 'インスタンス作成時に引数に受け取ったパラメーターが正しい順序で指定されること' do orignal_method = SyncRequest.method(:new) allow(SyncRequest).to receive(:new) do |*args| expect(args.size).to eq 4 expect(args[0]).to eq request_params[:table_name] expect(args[1]).to eq request_params[:column_name] expect(args[2]).to eq request_params[:value] expect(args[3].class).to eq Hash orignal_method.call(*args) end post sync_path, params: request_params end end end end
補足: orignal_methodについて
allow
とreceive
で振る舞いをオーバーライドすると、本来の振る舞いが失われるため戻り値が変わってしまったり、インスタンス変数の更新がされなかったり...などの問題が発生します。
今回の場合だとorignal_method.call(*args)
を記述しないと、newからSyncRequest
クラスのインスタンスが返らないためexecの呼び出しに失敗します。
Failure/Error: ins.exec NoMethodError: private method `exec' called for true:TrueClass ins.exec
戻り値の作成が複雑な時や、newをオーバーライドした状態でブロック内部でSyncRequest.new
を実行するとスタックを食い潰してしまったりと考慮しないといけない事が意外と多いです。
なので、汎用的に使えるorignal_method.call(*args)
を採用しています。
allow(SyncRequest).to receive(:new) do |*args| SyncRequest.new(*args) end # Failure/Error: SyncRequest.new(*args) # SystemStackError: # stack level too deep
最後に
冒頭でコントローラーの責務外の単体テストをコントローラーの単体テストに書くべきではないと書きましたが、それ以外にもコントローラーの単体テストをファットにしない方が良い理由の1つにパフォーマンスの問題があります。
ライブラリやモジュールの単体テストはシンプルにプログラムを呼び出すだけなので、軽量な処理だと考えることができます。 しかしコントローラーの場合、バックグラウンドでサーバーを立ち上げているかもしれないし、大量のモックを生成しているかもしれません。 そのため、ライブラリやモジュールの単体テストと比べると重い処理であり、一般的にはパフォーマンスが悪いと考えられます。
少しでも「ええな〜」と思ったらはてなスター・はてなブックマーク・シェアを頂けると励みになります。