transientとafter(:build)
はじめに
こんにちは!大ちゃんの駆け出し技術ブログです。
RSpecで少し詰まった箇所があったので、今回はそちらを記事にします。今回の記事は試験的に少し雑なアウトプットをさせていただきます。いつもの記事も雑かもしれませんが、、、
理由は下記2つです。
- そもそも読者想定は初学者ですが、ある程度勉強を進めているので1からアプリを用意するということはもうすでに必要ないことなのかなと思った。
- 2月は毎日投稿を目指していますが、仕事しながらだとRUNTEQの課題や転職用のポートフォリオのために使う時間が撮れないと感じた。
2つ目の理由についてはネガティブな理由ではあるものの、やはり投資時間を分散させるために試験的に行ってみたいと思います。
トピックは記事のタイトル通り、transient
とafter(:build)
です。
RSpecを書く上でとっても役に立つと思ったので是非みなさんも使ってみてください。
テストの概要
早速本題に入らせていください。
RUNTEQの応用編に現在取り組んでいますが、その課題でRSpecを作成する課題があります。
どのようなページで検証するのかと言うと、記事(Article)について、著者がいる記事Aと著者がいない記事Bが記事一覧ページにて検証します。
検証する内容は下記の通りです。
記事Aの著者で検索をすると記事Aだけが表示される。
このページには検索窓がありそれが下記の画像のような感じ。
この著者のセレクトボックスで著者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_A
とarticle_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
これが自分にとって初となるtransient
とafter(:build)
の出会いでしたww
なんだこの記述ってすごく思ったんですけど、理解するのは意外と簡単なんです!
transient
まずtransientから説明していきます!
let
でtrait
を指定すると、同時にtrait
のブロック内に存在するtransient
内のの値が取得できます。例えば、下記のようにcreate
に第三引数を渡さずにFactoryBotを呼び出す記述を書くとします。
let(:article_with_test_author) { create(:article, :with_author) }
このFactoryBotが呼び出されるとtransient
内の中の値、つまりauthor_name
とauthor_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)
の部分で確認することができます!
補足しておくと、let
のcreate
の第三引数であった: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
を使用した場合、let
の1回の呼び出しで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回も呼び出さなければなりません。すごい冗長ですよね、、、。
終わりに
今回の説明でtransient
とafter(:build)
の概要は何となく理解できたかなと思います。
たくさんのlet
の使用は冗長化につながりますので、1回のlet
の使用でアソシエーション関係にあるFactoryBotの作成は是非とも使ってみてください!
以上、以上、大ちゃんの駆け出し技術ブログでした!