Everyda Rails 備忘録


Everyda Rails- RSpecによるRailsテスト入門の重要な部分を備忘録としてまとめていく。
rspecの書き方だけではなく、どういう状況に対してテストを書くべきかというテストケースについてもまとめていく。

テスト全般で使える技法、慣行

describe User do
  # 姓、名、メール、パスワードがあれば有効な状態であること
  it "is valid with a first name, last name, email, and password"
  # 名がなければ無効な状態であること
  it "is invalid without a first name"
  # 姓がなければ無効な状態であること
  it "is invalid without a last name"
  # メールアドレスがなければ無効な状態であること
  it "is invalid without an email address"
  # 重複したメールアドレスなら無効な状態であること
  it "is invalid with a duplicate email address"
  # ユーザーのフルネームを文字列として返すこと
  it "returns a user's full name as a string"
end

上記のスペックは、以下のベストプラクティスに則っている。

  • 期待する結果をまとめて記述(describe)している。
  • example 1つにつき、結果を1つだけ期待している。
  • どのexampleも説明が明示的でわかりやすい。
  • 各exampleの説明は動詞で始まっている。shouldではない。

RSpecの記法、使い方

describe, context, before

describe,contextメソッドを用いることでサンプル(it)を大まかな状況、機能別にまとめて分類することができる。
この2つは技術的には効果可能であるが次のような使い分けをするのが良い。

describe

クラスやシステムの機能に関する概要、アウトラインを記述する。

context

特定の状態、状況に関する(条件分岐)概要、アウトラインを記述する。

この本の中での使われ方としてNoteモデルに着目すると、まずこのモデルには検索機能を持つのでこの機能についてのアウトラインをdescribeに記述。検索結果が返ってくる状態、返ってこない状態のそれぞれをcontextに記述する。

before

beforeブロックの中に処理を書くことでそれぞれのexampleに共通する事前処理をDRYにすることができる。

before(:each)

describe,contextブロックの中の各テストの前にそれぞれ毎回実行される。
before(:example)というエイリアスやbeforeのみでも良い。

before(:all)

describe,contextブロックの中の全テストの前に1回だけ実行される。

before(:suite)

テストスイート全体の全ファイルを実行する前に実行される。

(:all),(:suite)はテスト全体の実行時間を短縮するのに役立つが、テスト全体を汚染する可能性もあるので注意して使い、なるべく(:each)を使うのが良い。
それ以外にも(:each)ではデータ作成後ロールバックされるが、それ以外ではロールバックされないというRSpecの仕様がある。よって、(:all)などを使う場合は最後にデータを削除するのに気をつけなければならない。
Rspec Transaction

let, let!

beforeに変わる事前処理の共通化方法に、letlet!がある。letは遅延評価なので、呼ばれるまで中の処理が行われない。故に、参照されないままだとデータベースへの保存はされない。逆にlet!は即時評価であり、ブロック文が即時に評価されるため、exampleで参照されなくてもブロック内の処理はすでに行われている状態になる。

beforeとの違い

インスタンス変数として参照するかどうかの違いにある。インスタンス変数で参照する場合は、タイポした場合でもエラーが発生せずnilが返されてしまうのでエラーの原因の特定がしにくい。逆にletを使った場合、タイポしてしまうとエラーを発生させてくれるので原因特定がしやすい。

失敗の集約

aggregate_failuresブロックの中に複数のエクスペクテーションを書くことで、途中エクスペクテーションが失敗したとしてもブロックの最後までエクスペクテーションを評価し続ける。
統合テストやリクエストスペックでは、1つのexampleの中で複数のエクスペクテーションを書くことがあるのでこの機能を使うことで複数のエラー詳細を確認することができる。

テストにおけるDRYの考え方

基本的にテストにおいてもDRY原則は守るべきものです。
しかし、例えば大きなスペックファイルの中の上の部分でbeforeによる共通処理をDRYさせることによって、その共通処理を確認するために頻繁にスクロールさせている場合はDRYに忠実に従うのではなく、可読性を求めて重複したコードを書いても良い。

モデルスペック

モデルをテストすることはアプリケーションロジックの堅牢に繋がる。モデルをテストする時は以下の3つのケースを含めるのが良い。

  • 有効な値の場合はモデルの状態が有効(valid)になっていること
  • バリデーションに引っかかるケースの時はモデルの状態が無効(invalid)になっていること
  • クラスメソッドとインスタンスメソッドが期待通りに動作すること

1つ目は正常系、2つ目は異常(エラー発生)系のテストである。

テスト結果の精査

スペックを走らせた後の結果が全て成功したからといって満足してはいけない。このテスト結果が誤作動ではないことを確認するためには2つの手法がある。

  • toto_notにした時にテストが失敗するかどうか確認
  • アプリケーションのコードを変更してテスト結果が変更するか確認

2つ目についての具体例としてバリデーションのテストがある。バリデーションを行っているコードをコメントアウトした時にテスト結果が変わるかどうかの確認ができる。

フィーチャースペック

フィーチャースペックでは、複数のモデルやコントローラーがお互いにうまく動作しているかを確認するためのもので統合テストなどとも呼ばれる。

secario

今までのexampleはitで表現していたが、フィーチャースペックではscenarioで表現する。また、モデルスペックなどでは、1exampleにつき1エクスペクテーションが望ましかったが、フィーチャースペックではセットアップに時間がかかるなどの理由により1senarioに複数のエクスペクテーションを書いてもよい。

リクエストスペック

フィーチャースペックでは、ブラウザ上の操作のシミュレートを行なったが、プログラム上のやりとりは行わなかった。リクエストスペックでは、API関連のテストを対象とする。また、従来のコントローラスペックに置き換わるものとなる。

特徴

コントローラスペックとは違い、リクエストURLになんでも指定できる。つまり、他のコントローラーが処理するURLへのテストも可能である。
また、フィーチャースペックと同様に1example内に複数のエクスペクテーションを保持する傾向にある。これは、コントローラースペックよりも高いレベルでのテストであるからだと考える。個人的には、単体テストと統合テストの中間あたりなのではないかと思う。

モックとスタブ

モック

本物のオブジェクトのふりをするオブジェクトでテストのために使われる。
モックはデータベースとやりとりをしないため、データベースにアクセスする処理を減らすためによく使われます。

注意

モック化に関する有名な原則として、「自分で管理していないコードをモック化するな」がある。これは、ActiveRecordやサードパーティーの認証機能のオブジェクトをモック化してしまうとこれらのライブラリに変更があった場合にテストコードが報告してくれないかもしれない。
それでも、レートリミットを持つ外部APIとやりとりする必要があったりする場合など必要なときは存在する。

スタブ

オブジェクトのメソッドをオーバーライドし、事前に決められた値を返す。
呼び出されるとテスト用に本物の結果を返す、ダミーメソッド。
特にデータベースやネットワークを使う処理が対象になる。

Matcher

色々なMatcher(Github)

FactoryBot

FactoryBot Documentatin

注意点

FactoryBotではテスト中予期しないデータが作成されたり、テストが遅くなる可能性がある。このような問題が発生した場合は、まずファクトリが必要なことだけをしていることを確認し、コールバックなどを使う必要がある場合は、Traitの中で使うようにする。
また、createよりもbuildを使うことによりデータベースへの保存回数を減らせるのでパフォーマンス面に期待できる。