前回の記事ではPayjpを使った定期購入の実装について解説した。そこで今回は、Payjpを使った実装のテストについて書いてみたいと思う。
Payjpは外部APIなので、API呼び出し部分のモックを作ってテストを記述した方が良い。

対象となるアプリ

2つ前の記事で作成した単発購入機能のテストを書いていきたいと思う。(決して定期実装の方がめんどくさいからではない!!!………多分)

アプリの仕様

もう一度思い出してみる。まずフロントはチェックアウトを置くだけなので、テストの対象外。

<%# app/views/charges/new.html.erb %>

<script
  type="text/javascript"
  src="https://checkout.pay.jp/"
  class="payjp-button"
  data-key="#{ENV['PAYJP_PUBLIC_KEY']}"
  data-submit-text="購入する"
  data-text="カードを入力"
>
</script>

決済履歴を残しておく必要があるので、PaymentHistoryモデルを用意する。

# app/models/payment_history

# == Schema Information
#
# Table name: orders
#
#  id                                  :uuid             not null, primary key
#  amount                              :integer          default(0), not null
#  error_detail                        :string
#  error_message                       :string
#  status(決済ステータス)                :integer          default("before_payment"), not null
#  created_at                          :datetime         not null
#  updated_at                          :datetime         not null
#  charge_id(Payjp決済ID)               :string           default(""), not null
#  user_id                             :uuid             not null
#
# Indexes
#
#  index_orders_on_user_id  (user_id)

class PaymentHistory
  belongs_to :user

  enum status: {
    before_payment: 0, # 未決済
    completed: 1, # 決済完了
    failed: 2, # 決済失敗
  }
end

コントローラーには決済の処理だけを書いておいた。

# app/controllers/charges_controller.rb

class ChargesController < ApplicationController

  PAYJP_ERROR_CODE = {
    'invalid_number' => 'カード番号が不正です',
    'invalid_cvc' => 'CVCが不正です',
    'invalid_expiration_date' => '有効期限年、または月が不正です',
    'incorrect_card_data' => 'カード番号、有効期限、CVCのいずれかが不正です',
    'invalid_expiry_month' => '有効期限月が不正です',
    'invalid_expiry_year' => '有効期限年が不正です',
    'expired_card' => '有効期限切れです',
    'card_declined' => 'カード会社によって拒否されたカードです',
    'processing_error' => '決済ネットワーク上でエラーが発生しました',
    'missing_card' => '顧客がカードを保持していない',
    'unacceptable_brand' => '対象のカードブランドが許可されていません'
  }.freeze

  def create
    payment_history = current_user.payment_histories.create(status: :before_payment)
    response = Payjp::Charge.create(
      amount: params[:amount],
      card: params[:payjp_token],
      currency: 'jpy'
    )
    payment_history.update!(
      status: :completed,
      amount: response.amount,
      charge_id: response.id
    )
    render json: { message: '決済完了' }, status: 200
  rescue Payjp::PayjpError => e
    err = e.json_body[:error]
    payment_history.update!(
      status: :failed,
      error_message: err[:code],
      error_detail: PAYJP_ERROR_CODE[err[:code]]
    )
    render json: { message: PAYJP_ERROR_CODE[err[:code]] }, status: 400
  rescue StandardError => e
    payment_history.update!(
      status: :failed,
      error_message: 'failed_payment',
      error_detail: '何らかの理由で決済に失敗しました'
    )
    render json: { message: '何らかの理由で決済に失敗しました' }, status: 500
  end
end

Payjpの処理を記述しているのはコントローラーだけなので、Request Specを書いていく。

モックの作成

APIを呼び出さなくても期待した値が返却されるように、モック用のヘルパーを用意する。

# spec/support/payjp_mock_helpers.rb

module PayjpMockHelpers
  def charge_mock(payjp_charge_id)
    payjp_charge = double("Payjp::Charge")
    allow(Payjp::Charge).to receive(:create).and_return(payjp_charge)
    allow(payjp_charge).to receive(:id).and_return(payjp_charge_id)
    allow(payjp_charge).to receive(:amount).and_return(payjp_charge_amount)
  end
end

上記のヘルパーをrails_helperでincludeする。

# spec/rails_helper.rb

RSpec.configure do |config|
  # .......
  # .......
  # .......
  # .......

  config.include PayjpMockHelpers
end

モックが用意出来たらRequest Specを書いていく。

# spec/requests/charges_spec.rb

RSpec.describe 'charges', type: :request do
  describe 'POST /charges' do
    let(:user) { FactoryBot.create(:user) }
    let(:payjp_charge_id) { 'ch_xxxxxxxxxxxxxxxxxxxxxxxxxxxx' }

    before do
      charge_mock(payjp_charge_id)
    end

    it '決済出来る' do
      sign_in user # これはdeviseで用意されているhelper
      charge_params = {
        amount: 18000,
        payjp_token: 'tok_xxxxxxxxxxxxxxxxxxxxxxxx',
      }
      expect {
        post charges_path, params: charge_params, as: :json
      }.to change { user.payment_histories.count }.by(1)
      expect(response).to have_http_status(200)
    end
  end
end

以上でテストを記述出来た。

まとめ

Payjpを使った実装のテストを、モックの作り方も含めて解説した。
この記事では以上系のテストは行なっていないし、モックの作り方ももっと良いやり方があるかもしれないので、是非自分で調べてブラッシュアップしてほしい。