【RSpec】Active Hashを用いたカテゴリーでテストをパスする方法


https://teratail.com/questions/275207
結論この記事

今回は、active hashを用いたカテゴリーの際に、テストがパスしずらい問題を解決していきます

詳しくいうと、

コントローラーの単体テストで、

    it 'showアクションにリクエストすると,そのコーヒーの産地が表示' do
      get drink_path(@drink)
      expect(response.body).to include('マルチリージョン')
    end

このようなテストがパスできなかったです。

期待する動作としては、

この画像の右部分に,
「産地 マルチリージョン」
とあるので、上記のようなテストを書きましたが、
drinks/show.html.erb

                <% if @drink.region %>
                  <th class="region-title">産地 </th>
                   <td class="region-name"><%= @drink.region.name%> </td>             
                <% end %>

@drinks.region.name には、「マルチリージョン」という文字が入って欲しかったのですが、

上記のテストを実行した結果、

 Drinks GET #show showアクションにリクエストすると,そのコーヒーの産地が表示
     Failure/Error: expect(response.body).to include('マルチリージョン')

       expected "\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>Details | Coffee Passport</title>\n    \n    \n\n   ...script src=\"/packs-test/js/footer-c1b52f0e268f9a453b2e.js\"></script>\n   \n  </body>\n</html>\n\n" to include "マルチリージョン"
       Diff:

中略

       +              <tr class="item-region">
       +                  <th class="region-title">産地 </th>
       +                   <td class="region-name"> </td>             
       +              </tr>

このようなHTMLが返され、

<td class="region-name"> </td>

ここに @drink.region.nameが入るはずですが、
何も入ってなかったので、色々試しました。
色々試した結果は、「teratailに質問しようとした文」の部分に色々書いてあります。

該当のコード

drinks/show.html.erb

   <% if @drink.region %>
     <th class="region-title">産地 </th>
     <td class="region-name"><%= @drink.region.name%> </td>             
    <% end %>

models/region.rb

class Region < ActiveHash::Base
  self.data = [
    { id: 1, name: '---'},
    { id: 2, name: 'マルチリージョン' },
    { id: 3, name: 'ラテンアメリカ' },
    { id: 4, name: 'アフリカ' },
    { id: 5, name: 'アジア、太平洋' }
  ]
end

factories/drinks.rb

FactoryBot.define do
  factory :drink do
    name                {"TOKYOロースト"}
    price               {350}
    explain             {"これはコーヒーの説明です"}
    region_id           {2}

このようなactive hashを用いた構成の場合に、しっかり<%= @drink.region.name%>
が表示されてるか確かめたいので、

spec/requests/drinks_spec.rb

    it 'showアクションにリクエストすると,そのコーヒーの産地が表示' do
      get drink_path(@drink)
      expect(response.body).to include('マルチリージョン')
    end

と、テストを書いた。

しかし、これだと、先ほど説明した通りに上手くいかない

spec/requests/drinks_spec.rb

require 'rails_helper'
# bundle exec rspec spec/requests/tweets_spec.rb

RSpec.describe "Drinks", type: :request do
  before do
    @user = FactoryBot.create(:user)
    region_id = Region.find_by(name: 'マルチリージョン').id
    body_id = Body.find_by(name: "LIGHT(軽い)").id
    acidity_id = Acidity.find_by(name: "LOW(少ない)").id
    processing_id = Processing.find_by(name: "WASHED(水洗式)").id
    @drink = FactoryBot.create(:drink,
                                name: "TOKYOロースト",
                                price: 350,
                                explain: "これはコーヒーの説明です",
                                region_id: region_id,
                                body_id: body_id,
                                acidity_id: acidity_id,
                                processing_id: processing_id)

結論このように書けば上手くいく

models/region.rb

class Region < ActiveHash::Base
  self.data = [
    { id: 1, name: '---'},
    { id: 2, name: 'マルチリージョン' },
    { id: 3, name: 'ラテンアメリカ' },
    { id: 4, name: 'アフリカ' },
    { id: 5, name: 'アジア、太平洋' }
  ]
end

spec/requests/drinks_spec.rb

    region_id = Region.find_by(name: 'マルチリージョン').id

    @drink = FactoryBot.create(:drink,
                                name: "TOKYOロースト",
                                price: 350,
                                explain: "これはコーヒーの説明です",
                                region_id: region_id,

これで、うまいこと取得できて、テストをパスできる

多分これは冗長で、ベストプラクティスではないような気がするので、プロのRubistの皆さんにご意見をお伺いしたいと思うので、それを追記していきます。

teratailに質問しようとした文

このアプリのgithub

起きてる問題点はこれに近いです

発生している問題・エラーメッセージ

 Drinks GET #show showアクションにリクエストすると,そのコーヒーの産地が表示
     Failure/Error: expect(response.body).to include('マルチリージョン')

       expected "\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>Details | Coffee Passport</title>\n    \n    \n\n   ...script src=\"/packs-test/js/footer-c1b52f0e268f9a453b2e.js\"></script>\n   \n  </body>\n</html>\n\n" to include "マルチリージョン"
       Diff:

中略

       +              <tr class="item-region">
       +                  <th class="region-title">産地 </th>
       +                   <td class="region-name"> </td>             
       +              </tr>

 <td class="region-name"> </td>   

ここに、コーヒーの産地が表示されるはずですが、テストで返されたHTMLには表示されてないので、テストがパスできません。

原因は、

この画像の、この投稿の産地の部分の「マルチリージョン」とか 、コクの「LIGHT(軽い)」の部分とかが、上手く表示されてないです。

該当のソースコード

drinks/show.html

<% if @drink.region %>
<th class="region-title">産地 </th>
<td class="region-name"><%= @drink.region.name%> </td>
<% end %>

と、こんな感じで、drinks/showでは、
その投稿の産地の名前が表示される仕様になっています

region.rb

class Region < ActiveHash::Base
  self.data = [
    { id: 1, name: '---'},
    { id: 2, name: 'マルチリージョン' },
    { id: 3, name: 'ラテンアメリカ' },
    { id: 4, name: 'アフリカ' },
    { id: 5, name: 'アジア、太平洋' }
  ]

end

ここで、Active Hashを用いてることが原因となっております

drinks_spec.rb

    it 'showアクションにリクエストすると,そのコーヒーの産地が表示' do
      get drink_path(@drink)
      expect(response.body).to include('マルチリージョン')
    end

spec/factories/drinks.rb

FactoryBot.define do
  factory :drink do
    name                {"TOKYOロースト"}
    price               {350}
    explain             {"これはコーヒーの説明です"}
    region_id           {Region.all.sample}
    body_id             {Body.all.sample}
    acidity_id          {Acidity.all.sample}
    processing_id       {Processing.all.sample}
    likes_count         {2}
    association :user 
    association :region
    association :body
    association :acidity
    association :processing

    after(:build) do |drink|
      drink.image.attach(io: File.open('app/assets/images/ethiopia.jpg'), filename: 'ethiopia.jpg')
    end
  end
end

spec/factories/regions.rb

FactoryBot.define do
  factory :region do
  end
end

試したこと

この二つの記事を参考にして、

まず、factorybot同士でアソシエーションを組んでないから、@drink.region.nameが表示されないと思って、

spec/factories/regions.rb

FactoryBot.define do
  factory :region do
    association :drink
  end
end

と記述したら

死ぬほど長いエラーが表示され、SystemStackError stack level too deep
こんな感じのエラーが出ました。

次に

app/models/region.rb

class Region < ActiveHash::Base
  self.data = [
    { id: 1, name: '---'},
    { id: 2, name: 'マルチリージョン' },
    { id: 3, name: 'ラテンアメリカ' },
    { id: 4, name: 'アフリカ' },
    { id: 5, name: 'アジア、太平洋' }
  ]
  + include ActiveHash::Associations
   + has_many :drink
end

このように記述しました
一番最初のエラーと変わりませんでした。

factories/region.rb

FactoryBot.define do
  factory :region do
    2      {'マルチリージョン'}
  end
end

このように記述したら

SyntaxError:
  /coffee_passport/spec/factories/regions.rb:3: syntax error, unexpected '{', expecting end
      2      {'マルチリージョン'}
             ^

このようなエラーになりました。

factores/drinks.rb

    region_id           {Region.second}

このように記述しなおしたら

      Failure/Error: region_id           {Region.second}

      NoMethodError:
        undefined method `second' for Region:Class
        Did you mean?  send

とエラーが表示されました。