はじめに
payjpなどの外部APIと通信するRailsアプリの場合、
テストする時には実際に通信が走ってしまっては困ります。
今回は、ダミー(テストダブル)を作成して、ダミーのモックを返すようにすることで、コントローラのテストを実装します。
実装のサンプルコードだけ知りたい場合は、【サンプルコード】の項目までスクロールしてください。
普通にテストを実装すると
コントローラーのテストでは一般に以下を実装します。
- コントローラーのアクションに対応するビューが正しく表示されるかどうか
- リダイレクトが正しく行われるかどうか
- インスタンス変数が正しく定義されているかどうか
もし、以下のようなコントローラーがあった場合について考えます。
def index
customer = Payjp::Customer.retrieve("cus_xxxxxxxxxxxxx")
end
今回はindexのアクションはindexのビューが表示されることをテストします。
コントローラのスペックでは以下のようになります。
it "cardの一覧画面(indexアクション)にアクセスすると、indexのビューが表示される" do
get :index
expect(response).to render_template("index")
end
しかしこのままでは実際のコントローラでPayjp::Customer.retrieve()のコードが走り、
実際のpayjpと通信してしまいます。
(テストなのに、payjpの顧客や売り上げを作成してしまうのはまずいですよね!)
payjpと通信してしまうのを回避する
通信を回避する方法として、allowというメソッドがあります。
使い方は以下のような感じです。
allow(Payjp::Customer).to receive(:create).and_return(true)
これはPayjp::Customer.create()という呼び出しがあった際に、trueを返すというものです。
これを使用すると、実際に通信が走ってしまうのを回避できると思います。
モックとは
モックとは、本物のように振る舞う偽物、結論以下のようなものです。
payjp_customer = double("Payjp::Customer")
先ほど、allowは最終的にtrueを返しました。
Payjp::Customer.create()ではpayjpの顧客を返すのが理想なため、顧客を返すにはどうしたら良いかというのを考えます。
payjpの実際の顧客データは以下の通りです。
顧客データ
{
"cards": {
"count": 0,
"data": [],
"has_more": false,
"object": "list",
"url": "/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards"
},
"created": 1433127983,
"default_card": null,
"description": "test",
"email": null,
"id": "cus_121673955bd7aa144de5a8f6c262",
"livemode": false,
"metadata": null,
"object": "customer",
"subscriptions": {
"count": 0,
"data": [],
"has_more": false,
"object": "list",
"url": "/v1/customers/cus_121673955bd7aa144de5a8f6c262/subscriptions"
}
}
上記を丸々変数やメソッドにして返すようにしても良いのですが、rspecには
便利なメソッドが用意されています。
テストダブル - double()
です。
テストダブルはdouble("Payjp::Customer")
のように使用します。こうすると、Payjp::Customerオブジェクトのダミーを用意することができます。(つまりモックを作ることができる)
これを使用すると以下のようにできます。
payjp_customer = double("Payjp::Customer")
allow(Payjp::Customer).to receive(:create).and_return(payjp_customer)
一歩応用すると
別のパターンを考えます。
もしコントローラに以下のようなコードがあったとします。
customer = Payjp::Customer.create(....)
customer.cards.retrieve(....)
上記コードだとpayjpとの通信は、3回起きます。
- Payjp::Customer.create()
- customer.cards
- cards.retrieve()
なので、テストダブル、allow文は以下のようになります。
payjp_customer = double("Payjp::Customer")
payjp_list = double("Payjp::ListObject")
payjp_card = double("Payjp::Card")
allow(Payjp::Customer).to receive(:create).and_return(payjp_customer)
allow(payjp_customer).to receive(:cards).and_return(payjp_list)
allow(payjp_list).to receive(:retrieve).and_return(payjp_card)
ここで注目して欲しいのはallow文です。
allow文は通信の回避という側面以外に、メソッドの登録をしているとも取れます。
double("Payjp::Customer")
で作成したオブジェクトはPayjp::Customerオブジェクトですが、実際のオブジェクトとは異なり、メソッドなどが全く定義されていないです。
(double("Payjp::Customer").cardsとしてもcardsというメソッドはありませんとエラーが出るでしょう。)
allow(payjp_customer).to receive(:cards).and_return(payjp_list)
とすることでpayjp_customerというテストダブルに、cardsというメソッドを教えていると捉えてもオッケーです。
【サンプルコード】 モックを使用したテストの実装
コントローラーのサンプル
より実践的な一例です。(*実際はもう少し、createアクションが完結になるよう努力しています。)
サンプルコード
def create
Payjp.api_key = get_payjp_key
customer = Payjp::Customer.create
@credit = create_payjp_card(current_user, customer)
if @credit.save
redirect_to credits_path
else
render "new"
end
end
private
def create_payjp_card(user, customer)
customer.cards.create(card: token_parmas[:token])
credit = Credit.new(
user_id: user.id,
customer_id: customer.id
)
end
specのサンプル
サンプルコード
context "createアクションにアクセスした時" do
before do
payjp_customer = double("Payjp::Customer")
payjp_list = double("Payjp::ListObject")
payjp_card = double("Payjp::Card")
allow(Payjp::Customer).to receive(:create).and_return(payjp_customer)
allow(payjp_customer).to receive(:cards).and_return(payjp_list)
allow(payjp_list).to receive(:create).and_return(payjp_card)
allow(payjp_customer).to receive(:id).and_return("cus_xxxxxxxxxxxxx")
end
it "@creditが定義されていること" do
post :create, params: {token: "tok_xxxxxxxx"}
credit = create(:credit, user_id: user.id, customer_id: "cus_xxxxxxxxxxxxx")
expect(assigns(:credit).customer_id).to eq(credit.customer_id)
end
end
まとめ
モックの作成に、supportファイルを作成している方もいますが、私はdoubleを用いたテストの仕方をお勧めします。
ただし、allow文を使いすぎると、モックがきちんと作れているかのテストになってしまう気がします。railsではcontrollerテストがあまり推奨されていないように、他のテストでバリューを出すほうがいい気がします。
supportファイルでの実装
module PayjpMock
def self.payjp_mock_data
{
"cards": {
"count": 0,
"data": [],
"has_more": false,
"object": "list",
"url": "/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards"
},
"created": 1433127983,
"default_card": null,
"description": "test",
"email": null,
"id": "cus_121673955bd7aa144de5a8f6c262",
"livemode": false,
"metadata": null,
"object": "customer",
"subscriptions": {
"count": 0,
"data": [],
"has_more": false,
"object": "list",
"url": "/v1/customers/cus_121673955bd7aa144de5a8f6c262/subscriptions"
}
}
end
end
参考資料
「【Rails】PAY.JPによる決済機能のモックを活用したコントローラーテスト」
https://qiita.com/tiphp452/items/87042d1800af9a312be9
「Rails で Payjp を使って決済システムを導入する」
https://qiita.com/hirotakasasaki/items/794c920016ac7c33da74
「使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」」
https://qiita.com/jnchito/items/640f17e124ab263a54dd
「決済処理を統合テストしたい」https://muut.com/i/payjp/general:n8jubya6r7gxcp4398dp9d697ed