Open7
Ruby on Rails / RSpec スタイルガイド
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に依存しない単純な変数は避ける
- 一階層目の
describe
はKlassName
を必ず渡す。 - 二階層目の
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 だけを表示させるようにする。
- デフォルトだと ja.errors.format は
一階層目の describe は KlassName を必ず渡す。
二階層目の describe は必ず #method_name .method_name にする。それぞれクラスメソッド/インスタンスメソッドのみがわかる。
三階層目は必ず context にする。それ以上の階層は作らない。(場合によっては作る。作らないように努力する)
describe class / describe method / context when|with / it|specify|example
の原則
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はまた今度。
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に変更をしたときに返したくない情報を返してしまうかもしれない。
RSpecで返ってきたオブジェクトの値をかっちり目にチェックしたいとき
is_expected.to satisfy do |obj|
expect(obj).to have_attributes(id: 1)
expect(obj).to have_attributes(name: 'name')
end
FactoryBot.lint
で通らないfactorybotができないようにする。