RSpec Tips

概要

ONEのAPIはリリース当初からRuby on Railsで開発しており、少人数でメンテナンスを続けてきました。RSpecでテストを書くことは初期から続けており、現在カバレッジは87%です。RubyやRSpecの表現力を損なうことなく、シンプルにテストを書いていけいるようなTipsを紹介していきたいと思います。

  • Ruby 2.6.6
  • Ruby on Rails 6.0.4
  • RSpec 3.10.0

その他Railsで行っていること

  • Sidekiq
  • DBをmasterとreplicaに分割

Modelのテスト

簡単なModelのテストを使って説明していきたいと思います。 image_url: true の部分は独自のValidatorを作っています。

# == Schema Information
#
# Table name: brands
#
#  id         :uuid             not null, primary key
#  logo_url   :string
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class Brand < ApplicationRecord
  validates :logo_url, image_url: true, allow_nil: true
end

テストコードです。describeでattributeを指定し、それぞれのcontextでattributeの値を変更しています。

RSpec.describe Brand, type: :model do
  describe '#logo_url' do
    subject { brand.validate(:logo_url) }

    let(:brand) { build(:brand) }

    before do
      brand.logo_url = logo_url
    end

    context 'when logo_url exists' do
      let(:logo_url) { 'https://assets.wow.one/img/logo.jpg' }

      it { is_expected.to be_valid }
    end

    context 'when logo_url is nil' do
      let(:logo_url) { nil }

      it { is_expected.to be_valid }
    end

    context 'when logo_url is not image url' do
      let(:logo_url) { 'https://example.com' }

      it { is_expected.to be_invalid }
    end
  end
end

この部分がとてもシンプルになっているのがわかると思います。細かくどんなエラーになるかをテストするには不十分ですが、これで十分としています。

it { is_expected.to be_valid }

どう実現しているかと言うと、 subject { brand.validate(:logo_url) } で利用している validate メソッドを定義しています。

module RSpec
  class ActiveRecord::Base
    class Validation
      attr_accessor :errors

      def initialize(errors)
        @errors = errors
      end

      def valid?
        @errors.empty?
      end

      def invalid?
        !valid?
      end
    end

    def validate(key)
      valid?
      Validation.new(errors[key])
    end
  end
end

Requestのテスト

APIのschemaはOpenAPIを利用しています。途中から導入したので、全てのAPIが網羅されている状態ではありませんが、徐々にschemaと実装が合うことを保証していっています。interagent/committeewillnet/committee-rails を利用して、テストコードを書いています。

subject { post '/admin/color_themes', params: params }

context 'when the user is an operator', operation: true do # operator権限のstubをする
  it 'creates a color_theme' do
    expect { subject }.to change(ColorTheme, :count).by 1
    assert_response_schema_confirm 201 # commiteeでresponseの確認をする
  end

  context 'when the parameter is wrong' do
    let(:params) { {} }

		# よくあるエラーのレスポンスはshared_examplesを定義
    it_behaves_like 'follows openapi', status: 400
  end
end

エラー用のshared_examplesです。

RSpec.shared_examples 'follows openapi' do |status:, code: nil, message: nil|
  title = "follows openapi, status is #{status}"
  title += ", and error code is #{code}" if code
  title += ", and error message is #{message}" if message
  it title do
    subject
    expect(response).to have_http_status(status)
    expect(json['code']).to eq code if code
    expect(json['message']).to eq message if message
    assert_response_schema_confirm(status)
  end
end

開発中、 committee gem がボディーが空の304 (Not Modified)応答をバリデートしようとしてこけてしまう問題に当たったため、PRを出してマージしてもらいました。