Everyda Rails 備忘録

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

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

```ruby
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 １つにつき、結果を１つだけ期待している。
* どのexampleも説明が明示的でわかりやすい。
* 各exampleの説明は動詞で始まっている。shouldではない。

# RSpecの記法、使い方

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

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

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

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

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

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

### before(:all)
`describe`,`context`ブロックの中の全テストの前に１回だけ実行される。

### before(:suite)
テストスイート全体の全ファイルを実行する前に実行される。

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

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

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

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

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


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

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

１つ目は正常系、２つ目は異常（エラー発生）系のテストである。

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

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

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

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

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

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

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

# モックとスタブ

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

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

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


# Matcher
[色々なMatcher(Github)](https://github.com/rspec/rspec-expectations)

# FactoryBot

[FactoryBot Documentatin](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md)

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