テストコードについて(まとめ)
TECH CAMPカリキュラムで学習したテストコードについて、
詳細を自分用にまとめ。
特に単体テストコードについてはかなり詳しくアウトプットしたので、
これを見ればテストコードの基礎は一通り把握できるはず。
【テストコード】
アプリケーションがきちんと動作するかどうかを、手動ではなくコードで確認する方法。
・ミスや抜け漏れ防止
・負荷を低減
・アプリケーションの仕組みを把握できる
といったメリットがある。
Ruby on Railsでは、RSpec(アールスペック)というGemを使う事で
便利に実装出来る。
----------------------------------------------------------------
【テストコードの種類】
正常系:ユーザーが開発者の意図通りに操作を行った際の挙動確認
異常系:意図と異なる操作を行った場合の挙動確認
の主に2種類がある。
また、それぞれに
単体テストコード:コントローラーやモデルなどの機能ごとに問題がないか確認
結合テストコード:ユーザーが行う一連の流れを確認
がある。
----------------------------------------------------------------
【テストコード作成の流れ】
RSpecのGemを追加(Gem Fileのdevelopment testに記述)
→bundle install
→rails g rspec:install(アプリケーションにRSpecをインストール)
→テストコード結果をターミナルで可視化する為にrspecファイルを編集(.rspecに--format documentationと追記)
→rails g rspec:model user(Userモデルのテストファイル作成)
----------------------------------------------------------------
【describe】
テストコードのグループ分けをするメソッド。
どの機能に対してテストを行うかを区分けする。
【it】
同様にグループ分けをするメソッドで、どのような状況のテストを行うか、
より詳細に内容を明記する(discribeメソッドの中で記述する)。
※itで分けたグループをexampleと呼ぶ
----------------------------------------------------------------
【テストコードの実行】
テストコードの実行は、ターミナルでbundle exec rspecコマンドを使用する。
bundle exec rspec ディレクトリ/ファイル名
※itの中が空でもコマンド実行できる。結果は下記のように表示される。
----------------------------------------------------------------
【valid?】
バリデーション実行の結果、下記のように処理をするメソッド。
・エラーがない場合はtrueを返す
・エラーがある場合はfalseを返し、エラーメッセージを生成する。
バリデーションは本来データベース保存の前にしか実行されないが、
このメソッドがあれば任意のタイミングで実行させられる。
----------------------------------------------------------------
【expectation】
想定通りの挙動をするかを検証する構文で、結果をtrue/falseで返す。
(expect = 期待する)
ひな形はexpect().to matcher()
テストの内容によって、引数やmatcerを変更する。
expectの中には、検証で得られる挙動(または確認したい事項)が入る。
----------------------------------------------------------------
【matcher】
どのような挙動(結果)を想定しているかを記述する部分。
つまり、expect内に記述する内容はこうなるであろうという予測を書くものがmatcherである。
また、matcherには様々な種類がある。
代表的なマッチャは
・include→include(引数)とし、expect内にその引数があるかを確認する。
・eq→eq(引数)とし、expectの結果がその引数かどうかを確認する。
expect(['りんご', 'バナナ', 'メロン']).to include('メロン')→true
expect(1 + 1 ).to eq(2)→true
----------------------------------------------------------------
【エラーメッセージの抽出】
errors→インスタンスにエラーがある場合にエラー内容を返すメソッド。
full_messages→エラー内容から、エラーメッセージを取り出す
下記は、rails cを用いてvalid?・errors・full_messagesの3つを整理したもの。
user.errorsでエラー内容は分かるが、
expect内に記述できないのでfull_messagesで取り出す。
https://gyazo.com/e2055fdb1eeea4c58a894764a4feef53
ちなみに、date型カラムの値を書くときには'2000/01/01'のような記述となる。
クォーテーションで囲まないと2000 ÷ 1 ÷ 1になるので注意。
(最初は2000-01-01と書いたので、birth_day:1998となった)
----------------------------------------------------------------
【FactoryBot】
インスタンスを別ファイルで書き留めておけるGem。
複数回使うようなインスタンスを別ファイルに記述しておき、buildを行う事で簡単に引用作成出来る。
各テストコードごとにbuildするのではなく、beforeメソッドを使って事前にbuildしてインスタンス変数にしておく事も可能。
・FactoryBotの導入
Gemfileのgroup :development, test doの中にgem 'factory_bot_rails'の記述
→bundle install
→FactoryBot導入前にテストファイルを作成していた場合は、対応するFactoryBot用のファイルを手動で作成する(導入後にテストファイルを作成すると、対応ファイルは自動生成される)
↓手動で作成したファイル
・FactoryBotの設定方法
↓users.rbの中に下記のように記述すると、FactoryBotのベースが設定できる。
・設定後の使い方
テストコード用のファイルで、buildメソッドでベースのFactoryBotを呼び出す。
呼び出した時点ではベース設定のまま全ての項目が埋まっているので、テストしたい項目を次の行で任意に指定(今回はブランクに上書き)する。
----------------------------------------------------------------
【before】
上記では各exampleごとにFactoryBotを作成していて記述が重なっている
→beforeを用いて、1回の記述で済むようにすると効率的。
この際、beforeから変数を渡す場合、インスタンス変数にしなければならない。
また、beforeは、テストコードのグループ分けをする(つまりdescribeの前)に記述する。
----------------------------------------------------------------
【Faker】
設定する事で、Fakerメソッド内で用いた文字列や値をランダムなものにする事が出来るGem。
FactoryBotと組み合わせると、自動的に生成されるインスタンスの中身をランダムに出来る。
都度テスト用のユーザーネームやパスワードを打ち込まなくても自動で当て込んでくれるので効率的になる。
・Fakerの導入
Gemfileにgem 'faker'を記述してターミナルでbundle installを実行する。
※テストコードに用いるGemなので、FactoryBot同様にgroup :development, test doの中に記述する。
・Faker導入後の流れ
↓Fakerを用いて、下記のようにランダムな値を取得することができる。
これをFactoryBotのファイル内で同じように設定すればOK。
ちなみに、Fakerで誕生日をランダム生成する記述は下記から確認した。
その他の項目を生成する時も 、1つ前のdefaultのディレクトリから調べられるようである。
※後でレビューをもらって分かったこと
【Fakerの設定】
Fakerで「半角英数字でメールアドレスを作成」させる際、
password { Faker::Internet.password(min_length: 6) }
としていた。(上記の画像でもそうなっている)
しかしこの記述では、「数字のみ」や「英字のみ」のパスワードが生成されてしまい、テストが失敗することがある。
英数字混合のpasswordを想定した上でFakerを使用する場合は、
password { '1a' + Faker::Internet.password(min_length: 6) }
のように、確実にpasswordに英数字が含まれるように指定しなければならない。
(またFakerを用いず、直接値を書き込む形でも問題ない)
----------------------------------------------------------------
【deviseで自動設定されるバリデーション】
・validatable
メールアドレス→必須である・一意性(他と被ってはいけない)・@を含む必要がある
パスワード→必須である・6〜128文字であること
※下記はdevise導入時にデフォルトでバリデーションが備わっている
メールアドレスが必須であること
メールアドレスが一意性であること
メールアドレスは、@を含む必要があること
パスワードが必須であること
パスワードは、6文字以上での入力が必須であること
パスワードとパスワード(確認用)、値の一致が必須であること
----------------------------------------------------------------
【パスワードに半角英数字を必須とする実装】
deviseの基本機能には備わっていないようで、過去のカリキュラムを確認。
正規表現で実装できそうなことは分かったが、具体的な設定方法は確認できず。
Google検索で下記のURLを参考に、バリデーションに記述。
テストしたところ、英数字混合でない場合はパスワードが無効になった。
↓実際の記述。
formatは、withオプションで与えられた正規表現がマッチするかどうかを判定する。
↓テストコードの記述。
↓テスト実行時のターミナル。
※後でレビューをもらって分かったこと
【半角英数字に対するテスト】
「半角英数字での入力が必須」というテストコードを記述したが、正確性向上の為、「半角である必要がある」と「英数字である必要がある」は分けてテストした方が良い。
※後でレビューをもらって分かったこと
【パスワード(確認用)】
パスワード(確認用)は、データベースに保存されない仮想の属性のため、
バリデーションを設定する必要がない。
上記はpassword_confirmationのバリデーション削除前。
----------------------------------------------------------------
【全角かな/カナ/漢字】・【全角カナのみの場合】
同様の設定でOK。
正規表現の記述は下記のサイトが有用。
----------------------------------------------------------------
【FactoryBotに全角漢字・かな・カナを生成させる方法】
デフォルトでは備わっていない機能。
追加でGimeiというGemを導入して対処した。
↓Gemfileのdevelopment, :testの項目にgem 'gimei'を追記してbundle install
↓導入すると、下記のようにランダムな名前が作れるようになる。
https://gyazo.com/371e1e4b508f9cd467077dbeaaaf2c65
↓最後に、FactoryBotと組み合わせるためにfactoryのファイルを編集。
(transient do ~ end部分も忘れずに)
↓テストコードは下記のように記述する。
↓Gimeiは下記のサイトに詳細が説明されている。
----------------------------------------------------------------
【be_valid】
正常系テストで使うメソッド。
valid?の結果がtrueであることを期待するという意味。
(trueですよね?ということ)
「この○○はbe_validです」と記述し、エラーが出ないかをチェックする。
(○○は、例えば文字数制限内のメールアドレスやニックネームなどを入れる)
コードの書き方
expect(○○).to be_valid
→○○はvalid?がtrueですよね?という文章。
rails c→FactoryBotからuserをbuild→valid?でチェック→true
ということで、下記のテストがtrueになる。
----------------------------------------------------------------
【正常系と異常系の記述区分け】
正常系と異常系は、contextを用いて記述する場所を区分けする。
↓下記のように、describeの中にcontextの階層を作って分ける。
----------------------------------------------------------------
【整理】
正常系はexpectとbe_validを紐付けて、trueであることを確認する。
異常系はvalid?メソッドでエラー文を出力させて、エラー文をexpectと紐づける。
エラー文には様々な種類があり、binding.pryで途中で処理を止めて確認する手法をとる。
(ちなみに、binding.pryは複数記述するとその都度処理を止めることが可能)
----------------------------------------------------------------
【メールアドレスの一意性テスト】
メールアドレスが他ユーザーと重複する場合にエラーとなることについてのテスト。
この場合、FactoryBotで作成したユーザーの1人目を保存
→2人目のユーザーを作成し、1人目と同じメールアドレスを指定
→エラーが発生するがテスト
という流れとなる。
↓最初は@user.saveをしておらず、一意性エラーにならなかった。
(saveをしないとテーブルに保存されないので、重複にならない)
----------------------------------------------------------------
【異常系のテストコード】
バリデーションに引っかかるような動作をコードで記述
→発生するエラーコードの文章を予測して「このようなエラー文章になればOK」とあらかじめ作っておく
→エラーコードの文章を取得して、予測した文章とマッチしていればOK
=エラー時はきちんとバリデーションが働く
この際に活用するのがエクスペクテーションというテストコードで、expectメソッドとerrors_full_messagesメソッドを用いる。
----------------------------------------------------------------
【エラーコードの確認方法】
コンソールで○○.valid?
→falseと表示される
→○○.errors.full_messages
→エラー文が表示される
※○○にはインスタンスが入る
異常系テストで、エクスペクテーションを行うにあたりエラーコードを入力するが、その確認の為に上記方法を用いる。
nil→何も存在しない事(ユーザーやツイートが何もない時など)
' '→文字列が空の状態(テキストやメールアドレスが空の時など)
異常系テストでバリデーション確認をする際に、上記を使い分ける。
また、エクスペクテーションで使うincludeはsは付かないのでスペルに注意。
----------------------------------------------------------------
【エラーメッセージの重複】
例)名字が空欄の場合
→「空欄不可」「正規表現」の2つのバリデーションを設定していると、エラーメッセージも2つ表示される。
ビューにエラーメッセージを表示させる記述をしている場合、
2つのエラー文が出てきてしまう。
解消するためには、バリデーションへの追記が必要。
↓これを
↓こうする。
allow_blank :trueを記述することで、ブランクの場合はバリデーションを適用させないようにできる。
presence trueとformat~を同一の行に記述したままだと、presence trueにもallow~が適用されてしまうので注意。
↓下記のサイトを参考にした。
----------------------------------------------------------------
【バリデーションの可読性向上】
バリデーションの記述はwith_optionsを用いることでまとめられる。
可読性向上のために修正を行なった。
validates :nickname, presence: true
validates :family_name, presence: true
validates :family_name, format: { with: /\A[ぁ-んァ-ヶ一-龥々ー]+\z/ }, allow_blank: true
validates :first_name, presence: true
validates :first_name, format: { with: /\A[ぁ-んァ-ヶ一-龥々ー]+\z/ }, allow_blank: true
validates :family_name_furigana, presence: true
validates :family_name_furigana, format: { with: /\A[ァ-ヶー-]+\z/ }, allow_blank: true
validates :first_name_furigana, presence: true
validates :first_name_furigana, format: { with: /\A[ァ-ヶー-]+\z/ }, allow_blank: true
validates :birth_day, presence: true
上記を↓のように書き換えた。
with_optionsの中にwith_optionsを入れ子構造にすることもできる。
----------------------------------------------------------------
【コントローラーの単体テスト】
モデルの単体テストの場合は、インスタンスを生成して、モデルに規定した内容(バリデーションなど)がその通りに働いているかを確かめる。
対してコントローラーの単体テストは、あるアクションにリクエストを送ったとき、想定通りのレスポンスが生成されるかを確かめる
→このテストには、Request Spec(Rspecのうちの1つ)という手法を使う。
(RSpecが提供している、コントローラーのテストコードを書く為に特化した手法で、Rspecを導入していれば使える)
----------------------------------------------------------------
【createメソッド】
FactoryBotで生成した値をデータベースに保存する為に活用する。
テスト用のDBに値を一旦保存するが、1回のテストが終了するごとに
保存された値が全て消去となる。
(ActiveRecordのcreateメソッドとは異なる)
FactoryBot.create(:tweet)のように使う。
----------------------------------------------------------------
【get】
get ○○_pathのように使う事で、どこのパスにリクエストを送るかを決めることが出来る。
コントローラーの単体テストで、リクエスト送信部分として使用。
(対応パスはrails routesで確認する)
----------------------------------------------------------------
【response】
リクエストに対するレスポンスの事。
binding.pryで停止している時にコンソールで入力すると、
レスポンスとしてたくさんの情報が表示される。
またresponse.bodyとすると、ブラウザに表示されるHTMLの表示部分が取得できる。
----------------------------------------------------------------
【HTTPステータスコード】
HTTP通信の処理の結果を数字で示すもの。
100〜 処理継続中
200〜 処理成功
300〜 リダイレクト
400〜 クライアントエラー
500〜 サーバーエラー
response.status
→レスポンスの中からステータスコードを確認できる。
(binding.pryで停止時)
----------------------------------------------------------------
【System Spec】
結合テストコードを記述する為に活用する技術(仕組み)のこと。
大枠の記述はRSpecと変わらない。
----------------------------------------------------------------
【Capybara】
System Specを記述する為に必要なGem。
Gemfileに標準で記載され、導入済み。
----------------------------------------------------------------
【System Specの使い方】
rails g spec:system usersのコマンドでファイルを生成する。
(これにより、Specフォルダ内にSystemフォルダとusers.spec.rbファイルが作られる)
----------------------------------------------------------------
【ユーザー新規登録に関するexample(結合テストコード)】
大枠は下記の2つで、contextで分ける。
①ユーザー新規登録ができるとき
②ユーザー新規登録ができないとき
さらにそれぞれのケースをユーザー目線で細分化していく。
----------------------------------------------------------------
【visit】
visit 〇〇_pathで指定のページに遷移する。
----------------------------------------------------------------
【page】
visitで訪れたページの見える分だけの情報が格納されている。
(カーソルを合わせないと見れない文字列は含まれない)
----------------------------------------------------------------
【have_content】
expect(page).to have_content(‘〇〇’)で
訪れたページの中に〇〇という文字列があるかどうかを判断するマッチャ。
have_no_contentになると、逆に文字列が無いことを確かめるマッチャになる。
(異常系テスト時に使用)
----------------------------------------------------------------
【fill_in】
fill_in ‘フォームの名前’, with: ‘入力する文字列’
→記述してテストコードを実行すると、自動でフォームへ入力出来る。
※フォーム名・要素は検証ツールで確認する
確認方法:検証ツール→左上のカーソルマークのボタンを押す→そのフォームのラベル名(フォームのすぐ上にある表題のような部分)にカーソルを合わせる→<label></label>で囲まれた部分がフォーム名になる
ただし、label内にfor='id名'が記載されている必要がある。このforを記載することでlabelとformを紐づけるため。
また、この紐づけができている場合は、label部分をクリックしてもフォーム内の入力カーソルが活性化する。
----------------------------------------------------------------
【find().click】
find(‘クリックしたい要素’).click
→指定した要素を実際にクリック出来る。
fill_inの時と同じく検証ツールを起動→左上のカーソルマークのボタンを押す→
→クリックしたい要素を確認
例)要素が<input type="submit" name="commit" value="会員登録" class="register-red-btn" data-disable-with="会員登録">となっていた場合
find('input[name="commit"]').clickという記述でクリックできる。
----------------------------------------------------------------
【change】
expect{ 動作 }.to change { モデル名.count }.by(1)
→モデルのレコード数がいくつ変動するのかを確認できる。
つまり、レコードが保存されているかどうかをチェックするということ。
※expectはchangeマッチャでモデルのカウントをする時のみカッコの種類が変わるので注意。
上記のクリックを行なったことでuserモデルのカウントが上がったことを確かめる場合は、↓のように記述する。
----------------------------------------------------------------
【current_path】
現在いるページのパス。
expect(current_path).to eq(root_path)とすると
今いるページがroot_pathであることが確認できる。
----------------------------------------------------------------
【hover】
find(‘ブラウザの要素’).hoverとすると、
特定の要素にカーソルを合わせた時の動作を再現できる。
テストコードで、カーソルを合わせた時に特定の項目が出るかどうかを確認する時に用いる。
----------------------------------------------------------------
【have_link】
指定したリンクがあるかどうかを確認するマッチャ。
----------------------------------------------------------------
【have_selector】
指定したセレクターがあるかどうかを確認するマッチャ。
投稿編集のリンクがあるかどうかや、投稿した内容自体が反映されているかを確認する時に活用する。
結合テストコードで使用した。
----------------------------------------------------------------
【all(‘クラス名’)】
同一のページにある同じ名前の要素をまとめて取り出せる。
aii(‘クラス名’)[添字]とすると、取得する番号を指定できる。
テストコードで、1つのページに同じクラス名が複数存在する時に活用する。
----------------------------------------------------------------
【find_link().click】
a要素で表示されているリンクをクリックできる(a要素のみに用いる)