System Spec

 

おことわり

実装に関してはググればもっと上手く綺麗にまとめてある方の記事があるのでそちらをご覧ください。 未来の忘れた自分に対する説明の仕方なので要点だけ知りたい人には向きません。ごちゃごちゃしてます。(下手くそかよ)


必要最低限の記述の仕方は現場RailsとQiitaの記事を見れば大体わかる *雰囲気知るには'現場Rails'の方が個人的に分かりやすいかも(chapter5)

ただ、書いてないことも多いのであとは調べるしかない

  • 今回必要な単語
    • len
    • let!
    • fill_iv
    • click_button
  • やらないといけない事
    •  system_specファイルの設定
    • login処理
    • moduleを作る

  • 今回の要件(いいか?ちゃんと読めよ?!*100)

    タスクとユーザーに関するsystem specを作成し、下記要件のテストケースを作成してください。

    [正常系]
    ・ユーザーの新規作成、編集ができること
    ・ログインが成功すること
    ・ログインした状態でタスクの新規作成、編集、削除ができること
    ・マイページにユーザーが新規作成したタスクが表示されること
    
    [異常系]
    ・メールアドレスが未入力時にユーザーの新規作成、編集が失敗すること
    ・登録済メールアドレス使用時にユーザーの新規作成、編集が失敗すること
    ・フォーム未入力時にログインが失敗すること
    ・ログインしていないユーザーでタスクの新規作成、編集、マイページへの遷移ができないこと
    ・他のユーザーのユーザー編集、タスク編集ページへの遷移ができないこと
    

    共通の確認観点として「処理後の遷移先のパスがどこか」を確認してください。

    [正常系] では、下記の観点も確認してください。

    • 「ログイン・ユーザー新規登録の成功時に対応するフラッシュメッセージが表示されること」
    • 「タスクの新規作成・編集・削除が成功した場合、対応するフラッシュメッセージと更新したデータが画面上に表示されていること」

    [異常系] では、下記の観点も確認してください。

    • 「ログイン・ユーザー新規登録の失敗時に対応するフラッシュメッセージが表示されること」
    • 「タスクの新規作成・編集が失敗した場合、対応するフラッシュメッセージと更新しようとしたデータが画面上に表示されていないこと」

    [その他の要件]

    • login処理はmoduleを作成して共通化して、specファイル間で呼び出せるようにしてください。
    • spec実行時にブラウザの表示有無を切り替える設定を spec/support 以下に追加してください
    • 実行するテストケースを限定できる設定を spec_helper.rb の50行目辺りを確認して有効化してください。
    • テストデータの作成にはFactoryBotやletを利用してください。 

    また作成したspecがテストとして機能しているか、ControllerやViewのコードを変更した際にテストが失敗することを確認してください

  • 確認ポイント(ちゃんと読めよ??)

    • Task, User, UserSession のシステムテストが記載できていること
    • expectではなるべくDBへのアクセスを行わないこと(テスト実行時の時間的コストがかかるため)
    • FactoryBot と let, let! を利用してテストデータを作成していること 
    • ブラウザのON/OFFやテストケースのfocus実行などの設定を行っていること
    • login用のメソッドはmoduleとして共通化し、引数に渡したUserオブジェクトによって異なる権限のユーザでログインできるように定義すること

できる限り細かく噛み砕いてみます

そもそもSystem Specってなにだ?🌝 🍙


Railsアプリケーションの全体的なテスト。 ブラウザを通してアプリケーションの挙動を外部的に確認できる(現場Rails,190p) (192pのコラムも見たらいいかも)



(前回はModel Specだったので、その名の通りModelファイルに関するテストだった。)

「なるほ、全体テストすりゃいいんだな!」ってのが分かったが、

🤔「どこに書いたらいいんだ?」

【問題文よく読め】

ちゃんと要件に書いてあるんだよ 作成 と。

さらに今回のようなSystem Specを行いたい場合は、現場Rails(5-8 / 199p)とqiitaの記事を見ると書いてあるように

ディレクトリは自身で作成する

ターミナルでコマンドで作ってもいいし、vscodeとかならマウスでポチポチ🌱作ってもいい

rspec-rails 3.7の新機能!System Specを使ってみた - Qiita

 

🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🍙 🐤

🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀

必要な単語

let


  • letは変数のように使えるオブジェクトを定義することができます。(現場Rails-211,212p)

    ただ定義するだけでは実行されません。 コントローラーなどに作ったdef ●●のように"作ったけど使わないメソッド"と同じです。

    これはletを定義した後にletまたは定義名を使わないと作っても実行されません。

    • let(定義名) { 定義の内容 }

let!


  • letは使われないと実行されないですが、! をつけると常に呼び出されます。(現場Rails-214p)

    「呼び出される時と呼び出されない場合があるけど、データは作っておきたい」そんな時にも使えます

    使い所としてはbeforeの外に記述がある場合です。beforeが先に呼び出されてしまうと外に書いていたletは実行されなくなります。beforeの前に呼び出して欲しいのでlet!で定義します。

    !を使わずにbeforeの中に書いてもいいですが、別所でまた書かないといけなくなったりするので汎用性のためです。

    • let!(定義名) { 定義の内容 }

fill_in


  • fill_inはhtmlでいう<label>タグの<input>要素であるテキストフィールド(label)名を指定して、その中に値を入れてくれるメソッドです。(現場Rails-202p)

    fill_in 'ほにゃらら', with: 'フガフガ'
    # ↑ ラベル名          ↑初めて見た
    

    上記で言うと<input>部分の'ほにゃらら'というlabelの中に'フガフガ'という文字(値)を入れてくれます

    f:id:michimo_10:20210607140502p:plain



    よく見るメールフォームなら

    fill_in 'Email', with: 'フガフガ'
    

    f:id:michimo_10:20210607140506p:plain



    こんな感じ。

  • あとwithってのは「ここに書いた文字入れてくれるんやな」程度だったので少しだけ調べてみました

    Matching arguments

    説明にあるやつを翻訳すると

    Use with to specify the expected arguments. A message expectation constrained by with
    will only be satisfied when called with matching arguments. A canned response for an
    allowed message will only be used when the arguments match.
    
    																↓   ↓
    
    期待される引数を指定するにはwithを使います。withで制約されたメッセージの期待値は
    によって制約されたメッセージの期待値は、一致する引数で呼び出された場合にのみ満たされます。許可されたメッセージに対する定型応答は
    許可されたメッセージに対する定型応答は、引数が一致したときにのみ使用されます。
    

    f:id:michimo_10:20210607140558p:plain



    期待される引数を指定する

    なるほどな!

click_button


🧐 🤪 🤯 🧐 🤪 🤯 🧐 🤪 🤯 🧐 🤪 🤯 🧐 🤪 🤯 🧐 🤪 🤯 🧐 🤪 🤯

🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀 🌀

・その他の要件

- ログイン処理を作る(汎用性のUP)


  • ログインヘルパーを作る-パーフェクトRails(360p)

    ログイン機能は複数のテストで共有するので、ヘルパーメソッドとしてhelperファイルに書きますログインが必要なページでもテストが行えるようにテストコードでログインする機能を実装します

    イメージとしては「コントローラーじゃなくてモデルに書いとく」みたいな感じです。共通する場所に先に書いとけばあとはそのメソッドだけ使えるので、その方が汎用性上がるから。

ただ、イメージに近いけど軽く読んで今回はパス。

なぜこれをするのか?要件に書いてあるんだよ!🐻 <ダボが

一応「login module rspec」でググるとこういう記事が出てきたよ。とりあえず脳死でその通りにやってみるよ

Rspecでサインインメソッドを共通化して切り出す(devise使わないとき)[system spec][request spec] - Qiita

【Rails】はじめてのSystemSpec(RSpec) - Qiita

f:id:michimo_10:20210607140634p:plain



f:id:michimo_10:20210607140643p:plain

コメントアウトを解除したお。これ何をしてるかっていうと

"spec/support/以下が読み込めるようにパスを渡してます"

  • 例えば。。

    よくプロフィール写真を設定しないときにデフォルトで👤 こういった表示がされるように👤 ⇦の画像がどこかのディレクトリにあるんだけどそこから引っ張ってくる為に書かれたりするよ。「ここにある画像の場所はこのディレクトリだよ!」と言う道筋を表す書き方をパスを通すと言います。

    書き方は ' ' と , で区切ってますが / でも大丈夫だ(と教えてもらった、まじ神。あざす)

  • ログインするのはuserなので中にはloginする為のモジュール(module)を作成します.
  • module LoginModule # LoginModuleという名前にしたがなんでも良い
      def login(user) # ログインするメソッドを定義する、以下内容
        visit login_path #ログイン画面にアクセスするよ
    		click_link 'Login' # Loginリンクをクリックするよ
        fill_in 'Email', with: user.email # Emailラベル内にloginの引数(user)のメールを入れる(やりたい事が変わる時があるのでメアドは都度入れる必要がある)
        fill_in 'Password', with: 'password' # Passwordラベルにpassword って入れてくれるよ
        click_button 'Login' # 最後にLoginボタンを押してくれるよ
      end
    end
    # rails_helper.rbの下に、このLoginModuleをinclude(組み込む)してあげると、
    # rails_helperをrequireしているspecファイル内でこのlogin(user)メソッドが使えるようになる
    
  • 上で書いたモジュールが読み込まれるようにrails_helper.rbに追記します

    #一番下の方
    # arbitrary gems may also be filtered via:
    # config.filter_gems_from_backtrace("gem name")
      config.include FactoryBot::Syntax::Methods # spec.rb内でletを定義する中で FactoryBot. を省略してくれる
      config.include LoginModule # login_module.rbに記載した内容のmoduleを使えるようにする(今回はLoginModule)
    end
    

この辺分かりにくいけど、上のをやっておかないと下の"spec/support 以下に"繋がらないよね


Capybara

次に行く前にCapybaraってなんだ ↓

Capybaraチートシート - Qiita


 

spec実行時にブラウザの表示有無を切り替える設定を spec/support 以下に追加してください

??? 🤔 ???

このブラウザ表示の有無って辺りの言い回しがふんわりしててわからん。 なんとなくカピバラが関係してる感だけある

あ〜…

カピバラがドライバを設定してくれるので(現場Rails194p)

chome_headlessをドライバとして使ってくれるんやな(小並感)

下記記事参照

【暫定版】Rails 5.1のSystemTestCaseでHeadlessモードのChromeを使ってみる - Qiita

現場Rails(194p)にはgemの記載がないので調べてくうちに以下を発見したのでやってみましたが、「パスを通せ」っていう余計なエラーが出ます

group :test do
	gem 'capybara'
		  … # ↓ こいつ
  gem 'selenium-webdriver'

# エラー出るので以下の様に書きましょう

	gem 'webdrivers'

Rspecの設定(SystemSpecの導入、実行時にブラウザ表示、非表示の切り替え設定) · Issue #5 · diveintocode-corp/rails_exam01_bugfix

  • こんがらがってるけど脳死でsupportの下にcapybara.rb作って中に記載するよ

    RSpec.configure do |config| # 現場Rails194pに書いてある内容
      config.before(:each, type: :system) do
        driven_by :selenium_chrome_headless #GUIを起動しないでブラウザテストする
      end
    end
    

    (カピバラの動作を行う記述って意味でcapybara.rbというファイル名にしたんだと思うよ、だから別にhogehoge.rbでも良い)


headlessの意味


記事で書いてあるけどイメージつかん!ので以下

上の_headlessを記入しないで$ rspecをしちゃうとブラウザ立ち上がっちゃう。

f:id:michimo_10:20210607141015g:plain



_headless記入してるとブラウザは立ち上がらずにターミナルだけで完結する(エラーは気にしないで)

f:id:michimo_10:20210607141107g:plain



  • 疑問点

    現場Rails(194p)にはspec/spec_helper.rb内に require 'capybara/rspec' と書いてあるが、今回の課題にはそもそもcapybaraファイルがない。

    上のログイン処理内で作成したsupport/の下にcapybara.rbというファイルを作成し中に現場railsと同じ内容を記載したが、現場Rails真似て require 'capybara/rspec'の記載をしてターミナルでrspecコマンドを打つとエラーが出る(そもそもファイル無いから当たり前)

    今回のケースでは「ファイルが無いからrequire 'capybara/rspec'を読み込まなくてもいい」という判断をしたがspec_helper.rb内に記載せずに、わざわざsupport/capybara.rbに記載し、上の中でsupportを読み込むようにしたの何故だろうか。また何故capybaraファイルが無いのだろうか。と。


    解決への道


    • ちなみに現場Railsに書いてあった通りにやるとうまくいかなかった (triさんに助けて頂きました。ありがとうございます!)


実行するテストケースを限定できる設定を spec_helper.rb の50行目辺りを確認して有効化してください。

  • この辺のやつ

     

    f:id:michimo_10:20210607141341p:plainf:id:michimo_10:20210607141337p:plain



なぜか?理由は下の確認ポイント

・確認ポイントブラウザのON/OFFやテストケースのfocus実行などの設定を行っていること 

特定のテストケースを実行したい時のfocus: true - その辺にいるWebエンジニアの備忘録

↑あやふやなまま、なんでこれすんの?

>> 実務で膨大なテストがあるときに全てをテストすると時間がかかるので、自分がテストしたい部分だけをしたいときにfocasを使います



expectではなるべくDBへのアクセスを行わないこと(テスト実行時の時間的コストがかかるため)

これの意味するのは「letを使え」という事。

理由としては以下の記事内にある *現場Railsならchapter5-11(212p)

RSpecのletを使うのはどんなときか?(翻訳) - Qiita


🌱 🦤...🌱 🦤...🌱 🦤...🌱 🦤...🌿🐥 ...🌱 🦤...🌱 🦤...🌱 🦤...🌱 🦤...🌱


弾かれた場所

ログアウトのテストが詰まった

describe 'ログイン後' do
    context 'ログアウトボタンをクリック' do
      it 'ログアウト処理が成功する' do
        login(user)
        click_link 'Logout'  # ← click_button としていた
        expect(page).to have_content 'Logged out'
        expect(current_path).to eq root_path
      end
    end
  end
end

「ボタンのタグ」か「リンクの部分なのか」というのもきちんと書き分けないと

👷👷👷👷👷👷👷👷👷👷👷👷👷🏿👷👷👷👷👷👷👷👷👷👷

Users ログイン前 ユーザー新規登録 登録済のメールアドレスを使用 ユーザーの新規作成が失敗する(3行目)

context '登録済のメールアドレスを使用' do
        it 'ユーザーの新規作成が失敗する' do
     ここ→ about_user = create(:user) # 違うユーザーで作成し
          visit new_user_path # ユーザ新規作成ページにアクセス
     ここ→ fill_in 'Email', with: about_user.email # 違うユーザのメールがmodule内の既存ユーザのメールを使う
          fill_in 'Password', with: 'password'
          fill_in 'Password confirmation', with: 'password'
          click_button 'Update'
          expect(page).to have_content '1 error prohibited this user from being saved' # 表示されるエラー文
          expect(page).to have_content 'Email has already been taken' # 表示されるエラー文
          expect(current_path).to eq users_path # 新規作成ページにいること
          expect(page).to have_field 'Email', with: about_user.email # メールフィールド内に入力したメールアドレスが残っている
        end
      end

about_userという適当な変数作ってたけどこれじゃダメだった

existed_userという様にしないといけなかったのだが、existedと言いうのはRSpec内のマッチャと呼ばれる部分のメソッドであらかじめ用意されているものらしい(🍵このメソッドもたくさんある)

Method: RSpec::Matchers#exist


他の部分もマッチャやメソッドが原因だったりした🍵

context '登録済のメールアドレスを使用' do
          it 'ユーザーの編集が失敗する' do
            visit edit_user_path(user)
            about_user = create(:user) # ← ここ
            fill_in 'Email', with: about_user.email # ← ここ
            fill_in 'Password', with: 'hogepassword'
            fill_in 'Password confirmation', with: 'hogepassword'
            click_button 'Update'
            expect(page).to have_content("Email has already been taken")
            expect(page).to have_content('1 error prohibited this user from being saved:')
            expect(current_path).to eq user_path(user) # 失敗したらedeiできるけどidがないページであることを期待する
          end
        end

context '他ユーザーの編集ページにアクセス' do
          it '編集ページへのアクセスが失敗する' do
		        about_user = create(:user) # ← ここ
		        visit edit_user_path(abouot_user) # ← ここ
            expect(page).to have_content("Forbidden access.")
            expect(current_path).to eq user_path(user)
          end
        end
      end

ここでもabout_userとしていたがエラーが止まらない!しゅごい!

こういう場合はother_というメソッドを使うとよいので other_user に変更する

Method: RSpec::Expectations::MultipleExpectationsNotMetError#other_errors

さらにこんな感じでスクショも出してくれるので、どのあたりが間違っているかも一目瞭然

f:id:michimo_10:20210607141744p:plain



回答に書いた内容の説明(一部)


RSpec.describe 'UserSessions', type: :system do
  let(:user) { FactoryBot.create(:user) } #letでuserを定義します。その内容はFactoryBotのuserをcreateします

  describe 'ログイン前' do
    context 'フォームの入力値が正常' do
      it 'ログイン処理が成功する' do
        visit login_path #ログイン画面にアクセスします
        fill_in 'Email', with: user.email # letのuserのメールを渡します
        fill_in 'Password', with: 'password' # letのuserのpasswordを渡します
        click_button 'Login' # Loginボタンをクリックします
        expect(page).to have_content 'Login successful' # 画面(page)に'Login successful'があるのを期待します
        expect(current_path).to eq root_path # 今いる場所はroot(/) であるのを期待します
      end
    end
…
…
describe 'タスク編集' do
      let!(:task) { create(:task, user: user) } # beforeより先に読み込んで欲しい
      let(:other_task) { create(:task, user: user) }
      before { visit edit_task_path(task) }
…
…
context '他ユーザーの編集ページにアクセス' do
          it '編集ページへのアクセスが失敗する' do
            other_user = create(:user) # 違うユーザーで作成し
            visit edit_user_path(other_user) # 違うユーザーページにアクセスし
            expect(page).to have_content("Forbidden access.") # エラーが表示され
            expect(current_path).to eq user_path(user) # 失敗し自分のページであること期待する
          end
        end
…
…

.rspec内に

--format documentation
  • 記述がない場合

    すげーシンプルにテストしてくれる

    f:id:michimo_10:20210607141911p:plain



  • 記載がある場合

    describe、context、itの部分を丁寧に表示してくれる

    f:id:michimo_10:20210607141934p:plain




  • 後になって気付いたこと

    ヒントに書いてあったよ…

    テスト実行時のブラウザについて

    rails generate rspec:system で作成されるファイルでは driven_by で実行時のブラウザを指定しています。

    f:id:michimo_10:20210607142016p:plain



    この記載を削除し、他のファイルでブラウザの指定を一元管理してしまいましょう。

    f:id:michimo_10:20210607142039p:plain



今なら上の意味が少し理解できる。ナンだったんだ…🍛

自分で書くのは辛いです