20
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Rails】PayjpのRSpecテスト、モックを使用してコントローラテストを実装する

Last updated at Posted at 2020-03-19

はじめに

payjpなどの外部APIと通信するRailsアプリの場合、
テストする時には実際に通信が走ってしまっては困ります。
今回は、ダミー(テストダブル)を作成して、ダミーのモックを返すようにすることで、コントローラのテストを実装します。

実装のサンプルコードだけ知りたい場合は、【サンプルコード】の項目までスクロールしてください。

普通にテストを実装すると

コントローラーのテストでは一般に以下を実装します。

  • コントローラーのアクションに対応するビューが正しく表示されるかどうか
  • リダイレクトが正しく行われるかどうか
  • インスタンス変数が正しく定義されているかどうか

もし、以下のようなコントローラーがあった場合について考えます。

card_controller.rb
def index
  customer = Payjp::Customer.retrieve("cus_xxxxxxxxxxxxx")
end

今回はindexのアクションはindexのビューが表示されることをテストします。
コントローラのスペックでは以下のようになります。

card_controller_spec.rb
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)

一歩応用すると

別のパターンを考えます。
もしコントローラに以下のようなコードがあったとします。

controller
customer = Payjp::Customer.create(....)
customer.cards.retrieve(....)

上記コードだとpayjpとの通信は、3回起きます。

  • Payjp::Customer.create()
  • customer.cards
  • cards.retrieve()

なので、テストダブル、allow文は以下のようになります。

spec
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ファイルでの実装
support/payjp_mock.rb
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

20
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?