Open7

Ruby on Rails / RSpec スタイルガイド

Kohei SugiKohei Sugi

ModelのValidationのspecについて

app/models/post.rb

class Post < ApplicationRecord
  validates :title, presence: true
end

spec/models/post_spec.rb

RSpec.describe Post, type: :model do
  describe '#valid?' do
    subject { described_class.new(title: title) }

    context 'when title is present' do
      let(:title) { 'new post title' }

      it { is_expected.to be_valid }
    end

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

      specify do
        is_expected.to satisfy do |obj|
          expect(obj).to be_invalid
          expect(obj).errors.full_messages).to contain_exactly('タイトルを入力してください')
        end
      end
    end
  end
end

config/locales/ja.yml

ja:
  errors:
    format: '%{message}'
  activerecord:
    errors:
       models:
        post:
          attributes:
            title:
               blank: 'タイトルを入力してください'

こだわりポイント

  • 基本的にテストタイトルは書かない派。
    • メンテナンスする行が増えてしまう。contextとそれに紐づくletで全てが満たせるはず。
    • contextの中のletはcontextに依存しない単純な変数は避ける
  • 一階層目の describeKlassName を必ず渡す。
  • 二階層目の describe は必ず #method_name .method_name にする。それぞれクラスメソッド/インスタンスメソッドのみがわかる。
  • 三階層目は必ず context にする。それ以上の階層は作らない。(場合によっては作る。作らないように努力する)
  • contextの中にletとbeforeなどを入れるが、それがcontextを表すようにする。
    • 特にletはパラメータの違いを簡潔に理解するのを助けられるようにするインターフェースになるように意識する。
  • modelのinvalidなときは必ずエラーメッセージをテストする。
  • satisfyに渡して、複数のテストコードを書いちゃう。
  • arrayが返ってくるテストは contain_exactly が便利。
  • 1行で書けるテストは it { is_expected.to xxx } と書けるテストは itで書くけれど、複数のケースを書く場合はspecify を使うようにする。
    • specifyのときはいつもaggregate failuresがtrueになるようにできないかなぁ。
  • described_class は基本的に使わないけれど、クラス名を変更するのがあり得るそうな状況では置換する範囲を小さくするために使う
  • エラーメッセージはi18nの機能を利用する。
    • invalidな場合のエラーメッセージはテストする
    • railsが用意するvalidationの機能はテストする必要はないが、エラーメッセージのテストは必要
      • デフォルトだと ja.errors.format は %{attributes}%{message} となっていて、 を入力してください の述部を書く形になるけれど、日本語にこれは合わないので、 message だけを表示させるようにする。
Kohei SugiKohei Sugi

一階層目の describe は KlassName を必ず渡す。
二階層目の describe は必ず #method_name .method_name にする。それぞれクラスメソッド/インスタンスメソッドのみがわかる。
三階層目は必ず context にする。それ以上の階層は作らない。(場合によっては作る。作らないように努力する)

describe class / describe method / context when|with / it|specify|example の原則

Kohei SugiKohei Sugi

Request Specの書き方メモ (index)

RSpec.describe 'PostsRequest', type: :request do
  describe 'GET /posts'  do
    subject { get posts_url }

    context 'when posts does not exist' do
      specofy 'return empty' do
        is_expected.to eq '200'
        expect(response.json).to eq([])
      end
    end

    context 'when posts exists' do
      let!(:post) { create(:posts) }
      let(:expected_json) do
        [
          {id: post.id, title: post.title}
        ]
      end

      specify 'return posts' do
        is_expected.to eq '200'
        expect(response.json).to eq(expected_json)
      end
    end
  end
end

こだわりポイント

  • FooControllerだったら 'FooRequest' でstringで渡すか、'Foo' で渡すかどちらか。
  • 二段目の describe は必ずHTTPメソッドにurlのpathを書くようにしている
  • subjectはurlヘルパーで呼び出すことでroutesのテストも兼ねる
  • before { get posts_url } として、 subject { response } とするのもありだけど、subjectじゃなくてもbeforeでもいいが、subjectの方が発火タイミングがコントロールしやすいことが多い気がする(要検討)
  • subjectが発火したらresponse codeがstringで返ってくるらしい、 is_expected.to be_okって書けるとよりrspecっぽいけれど、そのためにはsubjectは使えない。書き方は統一できると便利だけど、とらわれないようにする
  • response.bodyはresponse.jsonというのをはやしている。基本的に全部のbodyをチェックしないと安心できない。https://github.com/yalab/action_dispatch-test_response-json を使う
  • テストタイトル書かない派なのに、書いているのは rspec-openapi でドキュメントを自動生成させているから。
  • let!はアサーションに使うので、beforeではなくlet!でデータを作る
  • createとupdateとshowはまた今度。
Kohei SugiKohei Sugi
RSpec.describe 'PostsRequest', type: :request do
  describe 'GET /posts'  do
    subject { get posts_url }

    context 'when posts does not exist' do
      specofy 'return empty' do
        is_expected.to eq '200'
        expect(response.json).to eq([])
      end
    end

    context 'when posts exists' do
      before { create(:posts) }

      specify 'return posts' do
        is_expected.to eq '200'
        expect(response.json).to satisfy do |json|
          expect(json.count).to eq 1
          expect(json[0]).to have_attributes(name: 'name')
        end
      end
    end
  end
end
  • let!を辞めて、beforeに。
  • let!を使っていた理由はassertionで使いたかったからだが、idやcreated_atなどの同的な要素以外のassertionを行う形に変更。
  • ただし、このテストの書き方だとresponseがちょっと変わっただけで落ちないので微妙。
    • 意図しないserializerやjbuilderに変更をしたときに返したくない情報を返してしまうかもしれない。
Kohei SugiKohei Sugi

RSpecで返ってきたオブジェクトの値をかっちり目にチェックしたいとき

is_expected.to satisfy do |obj|
  expect(obj).to have_attributes(id: 1)
  expect(obj).to have_attributes(name: 'name')
end
Kohei SugiKohei Sugi

FactoryBot.lint で通らないfactorybotができないようにする。