大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

transientとafter(:build)

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

RSpecで少し詰まった箇所があったので、今回はそちらを記事にします。今回の記事は試験的に少し雑なアウトプットをさせていただきます。いつもの記事も雑かもしれませんが、、、

理由は下記2つです。

  • そもそも読者想定は初学者ですが、ある程度勉強を進めているので1からアプリを用意するということはもうすでに必要ないことなのかなと思った。
  • 2月は毎日投稿を目指していますが、仕事しながらだとRUNTEQの課題や転職用のポートフォリオのために使う時間が撮れないと感じた。

2つ目の理由についてはネガティブな理由ではあるものの、やはり投資時間を分散させるために試験的に行ってみたいと思います。

トピックは記事のタイトル通り、transientafter(:build)です。

RSpecを書く上でとっても役に立つと思ったので是非みなさんも使ってみてください。

テストの概要

早速本題に入らせていください。

RUNTEQの応用編に現在取り組んでいますが、その課題でRSpecを作成する課題があります。

どのようなページで検証するのかと言うと、記事(Article)について、著者がいる記事Aと著者がいない記事Bが記事一覧ページにて検証します。

Image from Gyazo

検証する内容は下記の通りです。

記事Aの著者で検索をすると記事Aだけが表示される。

このページには検索窓がありそれが下記の画像のような感じ。

https://i.gyazo.com/7c0d3f4ea882d8489b2fd64347ee9acc.png

この著者のセレクトボックスで著者Aを選択肢検索すると記事Aだけが表示されるということです。

テストのシステムスペックファイル

よってシステムスペックのシナリオは以下の通りになります。

# spec/system/admin_article_searches_spec.rb
describe '著者検索' do
    let(:admin_user) { create(:user, :admin) }
        let(:article_with_author_A) { create(:article, :with_author, author_name: '著者A') }
      let(:article_with_author_B) { create(:article, :with_author, author_name: '著者B') }
    before do
      article_with_author_A
      article_with_author_B
      login_as(admin_user) # ポイント1
      visit admin_articles_path # ポイント2
    end
    context "著者Aで検索される時" do
      before do
        within('.form-inline') do
          select article_with_author_A.author.name, from: 'q[author_id]' # ポイント3
          click_button '検索'
        end
      end
      it '著者Aの記事のみが表示される' do # ポイント3
        expect(page).to have_content(article_with_author_A.title), '著者Aの記事が表示されていません'
        expect(page).not_to have_content(article_with_author_B.title), '著者A以外の記事が表示されています'
      end
    end
  end

letの部分の説明は今回の記事の肝ですので、後で説明します。最初にシナリオの流れだけざっくり説明します。

[ポイント1] login_as(admin_user)でまずは管理者画面にログインします。login_asメソッドはモジュールで独自に定義してあります。

# spec/support/login_support.rb
module LoginSupport
  def login_as(user)
    visit admin_login_identifier_path
    fill_in 'user_name', with: user.name
    click_button '次へ'
    expect(current_path).to eq admin_login_password_path
    fill_in 'user_password', with: 'password'
    click_button 'ログイン'
  end
end

[ポイント2] Article(記事)モデルとAuthor(著者)モデルの関係性は多:1です。Authorが複数の記事を持つということです。

ですので、セレクトボックスから著者の名前を取り出す方法はarticle_with_author_A.author.nameとなります。

[ポイント3] そして、検証部分では著者Aの記事だけが表示され、著者Bの記事は表示されないことを想定しています。

expect(page).to have_content(article_with_author_A.title), '著者Aの記事が表示されていません'
expect(page).not_to have_content(article_with_author_B.title), '著者A以外の記事が表示されています'

問題なのはletの部分です。

admin_userに関しては簡単です。createの第二引数でtraitを指定し、管理者ユーザーが作成されるように指定しています。UserモデルのFactoryBotの中身を見てみると、trait:adminの記述があるのが分かります。

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
・
・
    trait :admin do
      sequence(:name) { |n| "writer-#{n}" }
      role { :admin }
    end
・
・
  end
end

一方でarticle_with_author_Aarticle_with_author_Bはどうでしょうか。

let(:article_with_author_A) { create(:article, :with_author, author_name: '著者A') }
let(:article_with_author_B) { create(:article, :with_author, author_name: '著者B') }

createの第三引数にauthor_nameの指定がありました。自分はRSpecにおけるcreateの第三引数自体が初見だったので少し混乱しました。

ArticleのFactoryBotでtraitが:with_authorの部分を見てみると見慣れない記述があります。

FactoryBot.define do
  factory :article do

    trait :with_author do
      transient do
        sequence(:author_name) { |n| "test_author_name_#{n}" }
        sequence(:author_slug) { |n| "test_author_slug_#{n}" }
      end
  
      after(:build) do |article, evaluator|
        article.author = build(:author, name: evaluator.author_name, slug: evaluator.author_slug)
      end
    end

これが自分にとって初となるtransientafter(:build)の出会いでしたww

なんだこの記述ってすごく思ったんですけど、理解するのは意外と簡単なんです!

transient

まずtransientから説明していきます!

lettraitを指定すると、同時にtraitのブロック内に存在するtransient内のの値が取得できます。例えば、下記のようにcreateに第三引数を渡さずにFactoryBotを呼び出す記述を書くとします。

let(:article_with_test_author) { create(:article, :with_author) }

このFactoryBotが呼び出されるとtransient内の中の値、つまりauthor_nameauthor_slugも呼び出されます。

試しに、binding.pryで止めて値を確かめてみます。

transient do
  sequence(:author_name) { |n| "test_author_name_#{n}" }
  sequence(:author_slug) { |n| "test_author_slug_#{n}" }
  binding.pry # ここで止める!
end

するとそれぞれの値は以下のようになっていました。

[1] pry(#<FactoryBot::Declaration::Implicit>)> author_name
NameError: undefined local variable or method `author_name' for #<FactoryBot::Declaration::Implicit:0x00007f852fc39ba8>
Did you mean?  Pathname
from (pry):5:in `__pry__'
[2] pry(#<FactoryBot::Declaration::Implicit>)> author_slug
NameError: undefined local variable or method `author_slug' for #<FactoryBot::Declaration::Implicit:0x00007f852fc39ba8>
from (pry):6:in `__pry__'

ありゃりゃ、取得できていませんね。

ご安心ください。値自体はafter(:build)の部分で確認することができます!

補足しておくと、letcreateの第三引数であった:author_name"test_author_name_#{n}"の値ではなく別の値を格納したい時に指定します。今回の場合は"著者A"、"著者B"といった具合ですね。

after(:build)

続いてafter(:build)の中を覗いてみましょう。

after(:build) do |article, evaluator|
  article.author = build(:author, name: evaluator.author_name, slug: evaluator.author_slug)
end

evaluatorとは何なんでしょうか??

実はさっきのtrasient内の値はこの中に格納されています。

[2] pry(#<FactoryBot::SyntaxRunner>)> evaluator.author_name
=> "test_author_name_1"
[3] pry(#<FactoryBot::SyntaxRunner>)> evaluator.author_slug
=> "test_author_slug_1"

そしてこの値がarticle.authorというAuthorモデルの作成時に渡されています。

ブロック内の部分はとても単純ですよね。AuthorモデルのFactoryBotを作成しています。

build(:author, name: evaluator.author_name, slug: evaluator.author_slug)

だんだん見えて理解できてきましたでしょうか。

本来であれば、アソシエーション関係のあるFactoryBotの呼び出しをletのみで行う場合、letを2回使う必要がありました。今回の場合、記事が1つとその記事の著者が1つです。

しかし、transientを使用した場合、let1回の呼び出しで2つのFactoryBotを作成しています!

let(:article_with_author_A) { create(:article, :with_author, author_name: '著者A') }
let(:article_with_author_B) { create(:article, :with_author, author_name: '著者B') }

これによりletをシステムスペックファイルに記述する量を減らしているのです。今回の場合、著者Aの記事だけでなく著者Bの記事も必要だったため、letのみでFactoryBotを作成しようとすると、合計で4回も呼び出さなければなりません。すごい冗長ですよね、、、。

終わりに

今回の説明でtransientafter(:build)の概要は何となく理解できたかなと思います。

たくさんのletの使用は冗長化につながりますので、1回のletの使用でアソシエーション関係にあるFactoryBotの作成は是非とも使ってみてください!

以上、以上、大ちゃんの駆け出し技術ブログでした!