{"id":13471664,"url":"https://github.com/willnet/rspec-style-guide","last_synced_at":"2025-04-07T15:05:23.415Z","repository":{"id":30415207,"uuid":"33968070","full_name":"willnet/rspec-style-guide","owner":"willnet","description":"可読性の高いテストコードを書くためのお作法集","archived":false,"fork":false,"pushed_at":"2024-09-26T02:49:47.000Z","size":991,"stargazers_count":591,"open_issues_count":3,"forks_count":29,"subscribers_count":17,"default_branch":"master","last_synced_at":"2025-03-31T14:07:54.753Z","etag":null,"topics":["factory-girl","rspec","test"],"latest_commit_sha":null,"homepage":"https://qian-dao-zhen-yi.gitbook.io/rspec-style-guide/","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"Moya/Moya","license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/willnet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-04-15T02:13:58.000Z","updated_at":"2025-03-31T07:54:07.000Z","dependencies_parsed_at":"2024-01-16T07:22:16.046Z","dependency_job_id":"7b9b2109-f4b3-4571-88d7-b93184d02024","html_url":"https://github.com/willnet/rspec-style-guide","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willnet%2Frspec-style-guide","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willnet%2Frspec-style-guide/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willnet%2Frspec-style-guide/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willnet%2Frspec-style-guide/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/willnet","download_url":"https://codeload.github.com/willnet/rspec-style-guide/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247675597,"owners_count":20977376,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["factory-girl","rspec","test"],"created_at":"2024-07-31T16:00:47.984Z","updated_at":"2025-04-07T15:05:23.394Z","avatar_url":"https://github.com/willnet.png","language":"HTML","readme":"# RSpec スタイルガイド\n## これはなに\n\n可読性の高いテストコードを書くためのお作法集です。\n\nみんなの意見を取り入れることでより良いガイドにしていきたいと思っているので、次に当てはまる場合はどんどんIssuesやPull Requestを送ってください。\n\n- 疑問に思う点がある\n- ここに書かれていないお作法がある\n- 内容はいいけど表現がおかしい\n- もっといいサンプルコードがある\n\n[English Version](https://github.com/willnet/rspec-style-guide/blob/master/README_EN.md)\n\n## 前提\n\n- RSpec\n- FactoryBot\n\n## describeとcontext\n\n`describe`と`context`は同じメソッドだが、次のように使い分けることで何をテストしているのかをわかりやすくできる。\n\n- describe の引数にはテストの対象を書く\n- context の引数にはテストが実行される際に前提になる条件や状態を書く\n\n### 例\n\n```ruby\nRSpec.describe Stack, type: :model do\n  let!(:stack) { Stack.new }\n\n  describe '#push' do\n    context '文字列をpushしたとき' do\n      it '返り値がpushした値であること' do\n        expect(stack.push('value')).to eq 'value'\n      end\n    end\n\n    context 'nilをpushした場合' do\n      it 'ArgumentErrorになること' do\n        expect { stack.push(nil) }.to raise_error(ArgumentError)\n      end\n    end\n  end\n\n  describe '#pop' do\n    context 'スタックが空の場合' do\n      it '返り値はnilであること' do\n        expect(stack.pop).to be_nil\n      end\n    end\n\n    context 'スタックに値があるとき' do\n      before do\n        stack.push 'value1'\n        stack.push 'value2'\n      end\n\n      it '最後の値を取得すること' do\n        expect(stack.pop).to eq 'value2'\n      end\n    end\n  end\nend\n```\n\n## FactoryBotのデフォルト値\n\nFactoryBotを利用した場合、各モデルのデフォルトのカラム値を設定することになる。このとき、各カラムの値がすべてランダム値となるように設定を書くとよい。その上で、必要な値のみをテスト中で明示的に指定することにより、「このテストで重要な値はなにか」がわかりやすくなる。\n\n### よくない例\n\nアカウントが有効化されているかどうかをactiveカラムで管理しているとする。このような、有効／無効を表すカラムが固定されているケースはよく見かける。\n\n```ruby\nFactoryBot.define do\n  factory :user do\n    name { 'willnet' }\n    active { true }\n  end\nend\n```\n\n```ruby\nRSpec.describe User, type: :model do\n  describe '#send_message' do\n    let!(:sender) { create :user, name: 'maeshima' }\n    let!(:receiver) { create :user, name: 'kamiya' }\n\n    it 'メッセージが正しく送られること' do\n      expect { sender.send_message(receiver: receiver, body: 'hello!') }\n        .to change { Message.count }.by(1)\n    end\n  end\nend\n```\n\nこのテストは`User#active`が`true`であることが暗黙的な条件になってしまっている。`sender.active #=\u003e false`のときや`receiver.active #=\u003e false`のときにどう振る舞うかを伝えることができていない。\n\nさらに、このテストでは`name`を明示的に指定しているがこれは必要な指定なのだろうか？テストを読む人に余計なリソースを消費させてしまう、無駄なデータ指定はなるべく避けるのが好ましい。\n\n### よい例\n\n```ruby\nFactoryBot.define do\n  factory :user do\n    sequence(:name) { |i| \"test#{i}\" }\n    active { [true, false].sample }\n  end\nend\n```\n\n```ruby\nRSpec.describe User, type: :model do\n  describe '#send_message' do\n    let!(:sender) { create :user }\n    let!(:receiver) { create :user }\n\n    it 'メッセージが正しく送られること' do\n      expect { sender.send_message(receiver: receiver, body: 'hello!') }\n        .to change { Message.count }.by(1)\n    end\n  end\nend\n```\n\nこのテストだと「`User#active`の戻り値が`User#send_message`の動作に影響しない」ということが(暗黙的にであるが)伝わる。もし`User#active`が影響するような修正が加えられた場合、CIで時々テストが失敗することによって、テストが壊れたことに気付けるはずだ。\n\n大事なのは「テストに依存している値をすべてテスト中に明示している」ということなので、それが守られているのであればFactoryBotのデフォルト値を固定にしてもかまわない。\n\n### ランダム値が原因でテストが失敗したときの再現方法\n\n「各カラムの値をランダム値にしてしまうと、CIでテストが失敗した時に再現できないのでは？」という意見があるが、RSpecはデフォルトの設定で次のようにseed値を外から注入できるようになっている。\n\nspec/spec_helper.rb\n\n```ruby\nKernel.srand config.seed\n```\n\nなので、次のようにCIで失敗したときのseed値をrspecコマンドの`--seed`オプションに指定することで同じ状況を再現できる。\n\n```\nrspec --seed 1234\n```\n\n\n## FactoryBotで`belongs_to`以外の関連をデフォルトで作成しない\n\nFactoryBotでモデルを作成する際に、関連しているモデルも同時に作成することができる。\n\n対象の関連がbelongs_toであれば特に問題はないが、has_manyの関連を扱う場合には注意が必要になる。\n\n例として、UserとPostが一対多だったとしてFactoryBotでの定義を書いてみる。\n\n```ruby\nFactoryBot.define do\n  factory :user do\n    sequence(:name) { |i| \"username#{i}\" }\n\n    after(:create) do |user, evaluator|\n      create_list(:post, 2, user: user)\n    end\n  end\nend\n```\n\n`after(:create)`を使い、Userが作成された際に関連するPostも作成されるようにした。この定義を用いてUserが投稿したPostのうち、人気順で返す`User#posts_ordered_by_popularity`のテストを書いてみる。\n\n\n```ruby\nRSpec.describe User, type: :model do\n  describe '#posts_ordered_by_popularity' do\n    let!(:user) { create(:user) }\n    let!(:post_popular) do\n      post = user.posts[0]\n      post.update(popularity: 5)\n      post\n    end\n    let!(:post_not_popular) do\n      post = user.posts[1]\n      post.update(popularity: 1)\n      post\n    end\n\n    it 'return posts ordered by popularity' do\n      expect(user.posts_ordered_by_popularity).to eq [post_popular, post_not_popular]\n    end\n  end\nend\n```\n\n理解しづらいテストコードになった。このテストはUserのレコードを作成した時に、関連するPostを2つ作成することに依存している。また、[update](https://github.com/willnet/rspec-style-guide#updateでデータを変更しない)を利用してデータを変更しているため、最終的なレコードの状態を把握しづらくなっている。\n\nこれを避けるには、まず、デフォルトで一対多の関連レコードを作成することをやめるとよい。\n\n```ruby\nFactoryBot.define do\n  factory :user do\n    sequence(:name) { |i| \"username#{i}\" }\n\n    trait(:with_posts) do\n      after(:create) do |user, evaluator|\n        create_list(:post, 2, user: user)\n      end\n    end\n  end\nend\n```\n\n`trait`を利用し、デフォルトではPostを作成しないようにした。どんな値でも良いので関連先のPostがほしいときにはtraitを指定しUserを作成すれば良い。\n\n関連先はテスト中で明示的に作成するようにする。\n\n```ruby\nRSpec.describe User, type: :model do\n  describe '#posts_ordered_by_popularity' do\n    let!(:user) { create(:user) }\n    let!(:post_popular) { create :post, user: user, popularity: 5 }\n    let!(:post_not_popular) { create :post, user: user, popularity: 1 }\n\n    it 'return posts ordered by popularity' do\n      expect(user.posts_ordered_by_popularity).to eq [post_popular, post_not_popular]\n    end\n  end\nend\n```\n\nこれで、最初の例よりだいぶ見やすくなった。\n\n## 日時を取り扱うテストを書く\n\n注意: ただしく境界値分析を行いテストデータを生成すれば以下の手法は必要なくなる。以下の記述は補助的に使うのが良い。\n\n日時を取り扱うテストを書く場合、絶対時間を使う必要がないケースであればなるべく現在日時からの相対時間を利用するのが良い。その方が実装の不具合に気づける可能性が増すからだ。\n\n例として、先月に公開された投稿を取得するscopeと、そのテストを絶対時間を用いて記述する。\n\n```ruby\nclass Post \u003c ApplicationRecord\n  scope :last_month_published, -\u003e { where(publish_at: (Time.zone.now - 31.days).all_month) }\nend\n```\n\n```ruby\nrequire 'rails_helper'\n\nRSpec.describe Post, type: :model do\n  describe '.last_month_published' do\n    let!(:april_1st) { create :post, publish_at: Time.zone.local(2017, 4, 1) }\n    let!(:april_30th) { create :post, publish_at: Time.zone.local(2017, 4, 30) }\n\n    before do\n      create :post, publish_at: Time.zone.local(2017, 5, 1)\n      create :post, publish_at: Time.zone.local(2017, 3, 31)\n    end\n\n    it 'return published posts in last month' do\n      Timecop.travel(2017, 5, 6) do\n        expect(Post.last_month_published).to contain_exactly(april_1st, april_30th)\n      end\n    end\n  end\nend\n```\n\nこのテストは常に成功するが、実装にはバグが含まれている。\n\nテストを相対日時に変更してみる。\n\n```ruby\nrequire 'rails_helper'\n\nRSpec.describe Post, type: :model do\n  describe '.last_month_published' do\n    let!(:now) { Time.zone.now }\n    let!(:last_beginning_of_month) { create :post, publish_at: 1.month.ago(now).beginning_of_month }\n    let!(:last_end_of_month) { create :post, publish_at: 1.month.ago(now).end_of_month  }\n\n    before do\n      create :post, publish_at: now\n      create :post, publish_at: 2.months.ago(now)\n    end\n\n    it 'return published posts in last month' do\n      expect(Post.last_month_published).to contain_exactly(last_beginning_of_month, last_end_of_month)\n    end\n  end\nend\n```\n\nこのテストは、例えば3月1日に実行すると失敗する。常にバグを検知できるわけではないが、CIを利用することで、ずっと不具合が残り続ける可能性を減らすことができるだろう。\n\n## 日付を外部から注入する\n\n[timecop](https://github.com/travisjeffery/timecop)や[travel_to](https://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html#method-i-travel_to)などを使い現在時刻を変更しテストを実行するよりも、時刻を外部入力できるようにしたほうが堅牢なコードになる。詳細は次のブログエントリにまとまっている。\n\n[「現在時刻」を外部入力とする設計と、その実装のこと - クックパッド開発者ブログ](https://techlife.cookpad.com/entry/2016/05/30/183947)\n\n\n## beforeとlet(let!)の使い分け\n\nテストの前提となるオブジェクト(やレコード)を生成する場合、let(let!)やbeforeを使う。このとき、生成後に参照するものをlet(let!)で生成し、それ以外をbeforeで生成すると可読性が増す。\n\n次のような`scope`を持つ`User`モデルがあるとする。\n\n```ruby\nclass User \u003c ApplicationRecord\n  scope :active, -\u003e { where(deleted: false).where.not(confirmed_at: nil) }\nend\n```\n\nこのテストを`let!`のみを用いて書くと次のようになる。\n\n```ruby\nrequire 'rails_helper'\n\nRSpec.describe User, type: :model do\n  describe '.active' do\n    let!(:active) { create :user, deleted: false, confirmed_at: Time.zone.now }\n    let!(:deleted_but_confirmed) { create :user, deleted: true, confirmed_at: Time.zone.now }\n    let!(:deleted_and_not_confirmed) { create :user, deleted: true, confirmed_at: nil }\n    let!(:not_deleted_but_not_confirmed) { create :user, deleted: false, confirmed_at: nil }\n\n    it 'return active users' do\n      expect(User.active).to eq [active]\n    end\n  end\nend\n```\n\n`let!`と`before`を併用して書くと次のようになる。\n\n```ruby\nrequire 'rails_helper'\n\nRSpec.describe User, type: :model do\n  describe '.active' do\n    let!(:active) { create :user, deleted: false, confirmed_at: Time.zone.now }\n\n    before do\n      create :user, deleted: true, confirmed_at: Time.zone.now\n      create :user, deleted: true, confirmed_at: nil\n      create :user, deleted: false, confirmed_at: nil\n    end\n\n    it 'return active users' do\n      expect(User.active).to eq [active]\n    end\n  end\nend\n```\n\n後者のほうが「メソッドの戻り値となるオブジェクト」と「それ以外」を区別しやすく、見やすいコードとなる。\n\n※`let!(:deleted_but_confirmed)`のように名前をつけることで、どんなレコードなのか理解しやすくなると感じる人もいるかもしれない。しかしレコードに名前付けが必要であれば、単純にコメントとして補足してやればよいだろう\n\n## letとlet!の使い分け\n\n基本的に`let!`を推奨する。letの特性である遅延評価が有効に働く次のようなケースではletを使っても良い。\n\n```ruby\nlet!(:user) { create :user, enabled: enabled }\n\ncontext 'when user is enabled' do\n  let(:enabled) { true }\n  it { ... }\nend\n\ncontext 'when user is disabled' do\n  let(:enabled) { false }\n  it { ... }\nend\n```\n\n`let`で定義した値は呼ばれない限り実行されないので`let!`よりコストが低い、ということを利点として挙げている人もいるが、これを鵜呑みにすると「`let`で定義した値がテストケースによって使われたり使われなかったりする」状態になってしまう。これはテストの前提条件を理解するコストを上げ可読性を落とす。\n\n## 控えめなDRY\n\nDRYにする行為は常に善だと思われているかもしれないが、そうではない。例えばコードの重複を一つにまとめるということは処理の抽象化をするということで、状況や抽象化の仕方によってはDRYによって減らされたコストを上回るコストが発生することもある。\n\n### shared_examplesはよく考えて使う\n\nshared_examplesを利用するとコードの重複を削除できるが、書き方によってはかえって可読性を落とすことがある。\n\n\n例として、引数として渡された曜日に対応した分だけポイントを増やすメソッド`Point#increase_by_day_of_the_week`のテストを`shared_examples`を利用して書いてみる。shared_examplesの定義は別ファイルに書かれているとして、先にshared_examplesを利用する側だけのコードを見てみる。\n\n```ruby\nRSpec.describe Point, type: :model do\n  describe '#increase_by_day_of_the_week' do\n    let(:point) { create :point, point: 0 }\n\n    it_behaves_like 'point increasing by day of the week', 100 do\n      let(:wday) { 0 }\n    end\n\n    it_behaves_like 'point increasing by day of the week', 50 do\n      let(:wday) { 1 }\n    end\n\n    it_behaves_like 'point increasing by day of the week', 30 do\n      let(:wday) { 2 }\n    end\n\n    # ...\n  end\nend\n```\n\nどんな前提条件で結果として何を期待しているのか、これだけを見て理解できるだろうか。\n\n定義は次のようになる。\n\n```ruby\nRSpec.shared_examples 'point increasing by day of the week' do |expected_point|\n  it \"increase by #{expected_point}\" do\n    expect(point.point).to eq 0\n    point.increase_by_day_of_the_week(wday)\n    expect(point.point).to eq expected_point\n  end\nend\n```\n\nこのテストが読みづらいのは、shared_examplesを成立させるために必要な前提条件が多いことが一つ挙げられる。\n\n- テストされる主体である`point`\n- メソッドに渡される引数である`wday`\n- 期待する結果である`expected_point`\n\nまた、それぞれの定義が分散していることが挙げられる\n\n- 外側のlet(point)\n- `it_behaves_like`ブロック内のlet(wday)\n- `it_behaves_like`の第二引数(expected_point)\n\n可読性を上げるには、まず、適切に名前をつけることが考えられるだろう。\n\n```ruby\nRSpec.shared_examples 'point increasing by day of the week' do |expected_point:|\n  it \"increase by #{expected_point}\" do\n    expect(point.point).to eq 0\n    point.increase_by_day_of_the_week(wday)\n    expect(point.point).to eq expected_point\n  end\nend\n\nRSpec.describe Point, type: :model do\n  describe '#increase_by_day_of_the_week' do\n    let(:point) { create :point, point: 0 }\n\n    context 'on sunday' do\n      let(:wday) { 0 }\n      it_behaves_like 'point increasing by day of the week', expected_point: 100\n    end\n\n    context 'on monday' do\n      let(:wday) { 1 }\n      it_behaves_like 'point increasing by day of the week', expected_point: 50\n    end\n\n    context 'on tuesday' do\n      let(:wday) { 2 }\n      it_behaves_like 'point increasing by day of the week', expected_point: 30\n    end\n\n    # ...\n  end\nend\n```\n\n新しくcontextを作り、`wday`についての説明を加えた。そして、期待する結果である`expected_point`をキーワード引数として利用することで、実引数側に名前がつき、数値が何を表すのかすぐに理解できるようになった。\n\nしかし、そもそもこれは`shared_examples`を利用すべきケースなのだろうか？`shared_examples`を利用せずに書くと次のようになる。\n\n```ruby\nRSpec.describe Point, type: :model do\n  describe '#increase_by_day_of_the_week' do\n    let(:point) { create :point, point: 0 }\n\n    context 'on sunday' do\n      let(:wday) { 0 }\n\n      it \"increase by 100\" do\n        expect(point.point).to eq 0\n        point.increase_by_day_of_the_week(wday)\n        expect(point.point).to eq 100\n      end\n    end\n\n    context 'on monday' do\n      let(:wday) { 1 }\n\n      it \"increase by 50\" do\n        expect(point.point).to eq 0\n        point.increase_by_day_of_the_week(wday)\n        expect(point.point).to eq 50\n      end\n    end\n\n    context 'on tuesday' do\n      let(:wday) { 2 }\n\n      it \"increase by 30\" do\n        expect(point.point).to eq 0\n        point.increase_by_day_of_the_week(wday)\n        expect(point.point).to eq 30\n      end\n    end\n\n    # ...\n  end\nend\n```\n\n`it_behaves_like`での前提条件や引数が多くなればなるほど複雑度が増す。DRYにするメリットが複雑度を上回るかどうか、慎重に考えるべきだ。\n\n## スコープを考慮する\n### describe 外にテストデータを置かない\n\n例えば、次のような spec があるとする\n\n```ruby\ndescribe 'sample specs' do\n  context 'a' do\n    # ...\n  end\n\n  context 'b' do\n    let!(:need_in_b_and_c) { ... }\n    # ...\n  end\n\n  context 'c' do\n    let!(:need_in_b_and_c) { ... }\n    # ...\n  end\nend\n```\n\nこの場合、b と c で同じ前提条件を利用しているので、一つ上のレベルに移動してDRYにしようと考える人もいるかもしれない。\n\n```ruby\ndescribe 'sample specs' do\n  let!(:need_in_b_and_c) { ... }\n\n  context 'a' do\n    # ...\n  end\n\n  context 'b' do\n    # ...\n  end\n\n  context 'c' do\n    # ...\n  end\nend\n```\n\nしかし、これは良くない。'a' のコンテキストに、必要のない前提条件が含まれてしまうからだ。この例だけだとピンとこないかもしれない。このような let! が 10、context が 30 あって、どの let! がどの context に対応する前提条件なのかわからない状況を想像すると、下手に前提条件をまとめる怖さがわかるだろうか。\n\nもちろん、すべての context において、共通で使う前提条件であれば、まとめてしまうのは問題ない。\n\n\n### 各ブロックにおける前提条件の配置ルール\n\n- 各ブロックの前提条件は、その配下のすべての expectation で利用するもののみ書く\n- 特定の expectation のみで利用するものにおいては、その expectation に書く\n\n\n### 落穂拾い\n\n- 各ブロックにおける前提条件の配置ルール、の例外は次のようなケース\n- だが、基本的には各ブロック内で宣言するほうが望ましい\n\n```ruby\nlet!(:user) { create :user, enabled: enabled }\n\ncontext 'when user is enabled' do\n  let(:enabled) { true }\n  it { ... }\nend\n\ncontext 'when user is disabled' do\n  let(:enabled) { false }\n  it { ... }\nend\n```\n\n\n## 必要ないレコードを作らない\n\nパフォーマンスの観点から、レコードを作らなくてすむ場合は作らないようにしたい。\n\n```ruby\ndescribe 'posts#index' do\n  context 'when visit /posts' do\n    let!(:posts) { create_list :post, 100 }\n\n    before { visit posts_path }\n\n    it 'display all post titles' do\n      posts.each do |post|\n        expect(page).to have_content post.title\n      end\n    end\n  end\nend\n```\n\n「100件の投稿タイトルが表示できること」をテストしたい場合は別だが、ただ投稿タイトルを表示できているかチェックできればいい場合、明らかに無駄なレコードを作っている。\n\nこの場合の最小限のレコード数は1件である。\n\n```ruby\ndescribe 'posts#index' do\n  context 'when visit /posts' do\n    let!(:post) { create :post }\n\n    before { visit posts_path }\n\n    it 'display post title' do\n      expect(page).to have_content post.title\n    end\n  end\nend\n```\n\nモデルのユニットテストでも、作らなくてよいレコードを作っているケースはよくある。\n\n```ruby\nRSpec.describe User, type: :model do\n  describe '#fullname' do\n    let!(:user) { create :user, first_name: 'Shinichi', last_name: 'Maeshima' }\n\n    it 'return full name' do\n      expect(user.fullname).to eq 'Shinichi Maeshima'\n    end\n  end\nend\n```\n\n`User#fullname`はレコードが保存されているか否かに影響しないメソッドである。この場合は`create`ではなく`build`(もしくは`build_stubbed`)を使う。\n\n```ruby\nRSpec.describe User, type: :model do\n  describe '#fullname' do\n    let!(:user) { build :user, first_name: 'Shinichi', last_name: 'Maeshima' }\n\n    it 'return full name' do\n      expect(user.fullname).to eq 'Shinichi Maeshima'\n    end\n  end\nend\n```\n\nこのような単純なケースでは`User.new`を利用しても良い。\n\n## updateでデータを変更しない\n\nFactoryBotで作成したレコード中のカラムをupdateメソッドで変更すると、最終的なレコードの状態がわかりにくくなるし、テストに依存している属性もわかりにくくなるので避ける。\n\n```ruby\nRSpec.describe Post, type: :model do\n  let!(:post) { create :post }\n\n  describe '#published?' do\n    subject { post.published? }\n\n    context 'when the post has already published' do\n      it { is_expected.to eq true }\n    end\n\n    context 'when the post has not published' do\n      before { post.update(publish_at: nil) }\n\n      it { is_expected.to eq false }\n    end\n\n    context 'when the post is closed' do\n      before { post.update(status: :close) }\n\n      it { is_expected.to eq false }\n    end\n\n    context 'when the title includes \"[WIP]\"' do\n      before { post.update(title: '[WIP]hello world') }\n\n      it { is_expected.to eq false }\n    end\n  end\nend\n```\n\n`Post#published?`メソッドに依存している属性をすぐに理解することができるだろうか？updateはたいていFactoryBotのデフォルト値を「一番データとして多い形」に設定し、それを少し変更して使うために使われる。\n\nupdateは使用せず、[FactoryBotのデフォルト値](https://github.com/willnet/rspec-style-guide#factorybotのデフォルト値)に記載したようにデフォルト値をランダムに保つと良い。\n\n## letを上書きしない\n\n`let`で定義したパラメータを内側のcontextで上書きすると、[updateでデータを変更しない](https://github.com/willnet/rspec-style-guide#updateでデータを変更しない)で説明した例と同様に、最終的なレコードの状態がわかりにくくなるので避ける。\n\n```ruby\nRSpec.describe Post, type: :model do\n  let!(:post) { create :post, title: title, status: status, publish_at: publish_at }\n  let(:title) { 'hello world' }\n  let(:status) { :open }\n  let(:publish_at) { Time.zone.now }\n\n  describe '#published?' do\n    subject { post.published? }\n\n    context 'when the post has already published' do\n      it { is_expected.to eq true }\n    end\n\n    context 'when the post has not published' do\n      let(:publish_at) { nil }\n\n      it { is_expected.to eq false }\n    end\n\n    context 'when the post is closed' do\n      let(:status) { :close }\n\n      it { is_expected.to eq false }\n    end\n\n    context 'when the title includes \"[WIP]\"' do\n      let(:title) { '[WIP]hello world'}\n\n      it { is_expected.to eq false }\n    end\n  end\nend\n```\n\n## subjectを使うときの注意事項\n\n`subject`は`is_expected`や`should`を使い一行でexpectationを書く場合は便利だが、逆に可読性を損なう使われ方をされる場合がある。\n\n```ruby\ndescribe 'ApiClient#save_record_from_api' do\n  let!(:client) { ApiClient.new }\n  subject { client.save_record_from_api(params) }\n\n  #\n  # ...多くのexpectationを省略している...\n  #\n\n  context 'when pass  { limit: 10 }' do\n    let(:params) { { limit: 10} }\n\n    it 'return ApiResponse object' do\n      is_expected.to be_an_instance_of ApiResponse\n    end\n\n    it 'save 10 items' do\n      expect { subject }.to change { Item.count }.by(10)\n    end\n  end\nend\n```\n\nこのようなとき、`expect { subject }`の`subject`は一体何を実行しているのかすぐには判断できず、ファイルのはるか上方にある`subject`の定義を確認しなければならない。\n\nそもそも\"subject\"は名詞であり、副作用を期待する箇所で定義すると混乱を招く。\n\n`is_expected`を利用し暗黙の`subject`を利用する箇所と直接`subject`を明示する箇所が混在しており、どうしても`subject`を使いたい場合は、`subject`に名前をつけて使うと良い。\n\n```ruby\ndescribe 'ApiClient#save_record_from_api' do\n  let!(:client) { ApiClient.new }\n  subject(:execute_api_with_params) { client.save_record_from_api(params) }\n\n  context 'when pass  { limit: 10 }' do\n    let(:params) { { limit: 10} }\n\n    it 'return ApiResponse object' do\n      is_expected.to be_an_instance_of ApiResponse\n    end\n\n    it 'save 10 items' do\n      expect { execute_api_with_params }.to change { Item.count }.by(10)\n    end\n  end\nend\n```\n\n`expect { subject }`の時よりはわかりやすくなったはずだ。\n\n`is_expected`を利用していない場合は、`subject`の利用をやめて`client.save_record_from_api(params)`を各expectationにべた書きするのが良い。\n\n## allow_any_instance_ofを避ける\n\n[公式のドキュメント](https://rspec.info/features/3-12/rspec-mocks/working-with-legacy-code/any-instance/)にも書かれているが、`allow_any_instance_of`(`expect_any_instance_of`)が必要な時点でテスト対象の設計がおかしい可能性がある。\n\n例として、次のような`Statement#issue`のテストを書いてみる。\n\n```ruby\nclass Statement\n  def issue(body)\n    client = TwitterClient.new\n    client.issue(body)\n  end\nend\n```\n\n```ruby\nRSpec.describe Statement do\n  describe '#issue' do\n    let!(:statement) { Statement.new }\n\n    it 'call TwitterClient#issue' do\n      expect_any_instance_of(TwitterClient).to receive(:issue).with('hello')\n      statement.issue('hello')\n    end\n  end\nend\n```\n\n`expect_any_instance_of`を使ってしまったのは、`Statement`クラスと`TwitterClient`クラスが密結合しているのが原因である。結合を弱めてみる。\n\n```ruby\nclass Statement\n  def initialize(client: TwitterClient.new)\n    @client = client\n  end\n\n  def issue(body)\n    client.issue(body)\n  end\n\n  private\n\n  def client\n    @client\n  end\nend\n```\n\n```ruby\nRSpec.describe Statement do\n  describe '#issue' do\n    let!(:client) { double('client') }\n    let!(:statement) { Statement.new(client: client) }\n\n    it 'call TwitterClient#issue' do\n      expect(client).to receive(:issue).with('hello')\n      statement.issue('hello')\n    end\n  end\nend\n```\n\n`issue`メソッドを持つオブジェクトであれば、どのクラスでも`client`として扱えるように修正した。外部から`client`を指定できる作りにしたことで、将来的に`FacebookClient`など別のクライアントにも対応できるようになった。結合が弱まり、単純なモックオブジェクトでテストが記述できるようになった。\n","funding_links":[],"categories":["HTML"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwillnet%2Frspec-style-guide","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwillnet%2Frspec-style-guide","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwillnet%2Frspec-style-guide/lists"}