RSpecチートシート

前回: [FactoryGirlチートシート](http://qiita.com/morrr/items/f1d3ac46b029ccddd017)

FactoryGirlに続いてRSpecの使い方を調べてみました。

当文書は単にツールとしてのRSpecの使い方のまとめです。
TDD/BDDの概念やワークフロー、あるいは良いテストを書くための指針等にはふれていません。


概要
------------------------------------------------------------
RSpec = Rubyアプリケーションのテストのためのフレームワーク

- オープンソース / MITライセンス
- ビヘイビア駆動開発(BDD)をサポートする
- Spec(ification) = 仕様 = テストコード。コードの振る舞いを試験するためのテストと仕様を同じものととらえている
- 2017/07現在のバージョンは3.6系


導入
------------------------------------------------------------
Railsの場合はgem `rspec-rails`をインストールする。

```rb:Gemfile
# development環境でも利用する(し得る)点に注意
group :development, :test do
  gem 'factory_girl_rails'
  gem 'rspec-rails'

  # ...

end
```

```bash
$ bundle install
```

RSpecの最低限必要なファイル/ディレクトリ構成をRailsにインストールする。

```bash
$ rails g rspec:install
#>      create  .rspec                  # RSpecの設定ファイル
#>      create  spec                    # スペックを格納する
#>      create  spec/spec_helper.rb     # スペック記述のためのヘルパ
#>      create  spec/rails_helper.rb    # Rails固有のスペック記述のためのヘルパ
```

`bundle binstub`によってrspecコマンドをインストールする。
以降、rspecはこのコマンドによって実行する。
(`$ bundle exec rspec`でもいい)

```bash
$ bundle binstubs rspec-core

# 試しに実行してみる
$ bin/rspec 
#>No examples found.
#>
#>Finished in 0.00069 seconds (files took 0.252 seconds to load)
#>0 examples, 0 failures
```


ジェネレータ(`rails g(enerate)`)の利用時にspecファイルも同時に作成されるように設定変更する。

```diff:config/application.rb
module RspecDemo
  class Application < Rails::Application

+    config.generators do |g|
+      g.test_framework :rspec,
+        fixtures: true,         # モデル作成時にフィクスチャの作成を有効化(後述のfactory_girlが適用される)
+        view_specs: false,      # 以下、必要に応じて任意にトグルする
+        helper_specs: false,
+        routing_specs: false,
+        controller_specs: false,
+        request_specs: true
+    
+      # fixtureの代わりにfactory_girlを使うよう設定
+      g.fixture_replacement :factory_girl, dir: 'spec/factories'
+    end

  end
end
```

スペック内でFactoryGirlを使う場合に名前空間を省略できるように設定する。

```diff:spec/rails_helper.rb
RSpec.configure do |config|
+  config.include FactoryGirl::Syntax::Methods
end
```


基本的な運用
------------------------------------------------------------

### スペックファイルを配置する

スペックは、`rspec:install`によって作成された`spec/`以下に配置する。
前述の導入時の設定によりジェネレータが設定済みの場合は、モデル等の生成と同時にスペックファイルが自動的に作成される。

手動で配置する場合は、


- `spec/`内のファイルは、`app/`以下のテスト対象のrbファイルに対して1対1で対応するかたちで配置する。
    - たとえば、1つのモデルに対して1つのスペックファイル

- スペックファイルは接尾辞として`_spec.rb`を付ける。
    - `_spec`がないとスペックファイルとして認識されない


たとえば、`app/models/user.rb`に対するスペックは`spec/models/user_spec.rb`となる。

```
spec/
    models/         # モデルスペック
    controllers/    # コントローラスペック
    views/          # ビュースペック
    features/       # フィーチャースペック(結合テスト)
    support/        # 共通処理/定義/設定
```


###  振る舞いを記述する

スペックファイル内では、まずは`describe` / `it` メソッドによってテスト対象の__振る舞い__を列挙する。
(itによって定義される振る舞いは、RSpecでは__examples__と呼ばれる)

```rb:spec/models/user_spec.rb
# spec/rails_helper.rbを読む
# (rails_helper.rbはspec_helper.rbを読む)
require 'rails_helper'

# モデルUserに対するスペックを記述(describe)する
# 「ユーザは、」
describe User do

  # 「メールとアカウント名の設定時には有効となる」
  it 'is valid with a mail and account'

  # 「メールの未設定時は無効となる」
  it 'is invalid without a mail'

  # 「アカウント名の未設定時は無効となる」
  it 'is invalid without an account'

  # ...

end
```

rspecコマンドによってスペックを検証すると、具体的な振る舞いが実装されていないため保留状態となっていることが示される。

```bash
$ ./bin/rspec

#> User
#>   is valid with a mail and account (PENDING: Not yet implemented)
#>   is invalid without a mail (PENDING: Not yet implemented)
#>   is invalid without an account (PENDING: Not yet implemented)
#> 
#> Pending:
#>   User is valid with a mail and account
#>     # Not yet implemented
#>     # ./spec/models/user_spec.rb:5
#>   User is invalid without a mail
#>     # Not yet implemented
#>     # ./spec/models/user_spec.rb:8
#>   User is invalid without an account
#>     # Not yet implemented
#>     # ./spec/models/user_spec.rb:11
```

### 振る舞いを実装する

列挙した振る舞いに対してブロックを渡し、振る舞いの詳細を実装していく。

```rb:spec/models/user_spec.rb
require 'rails_helper'

describe User do

  it 'is valid with a mail and account' do
    user = User.new(
      mail: 'foo@example.com',
      account: 'foo'
    )
    expect(user).to be_valid
  end

  it 'is invalid without a mail' do
    user = User.new(mail: nil)
    user.valid?
    expect(user.errors[:mail]).to include("can't be blank")
  end

  it 'is invalid without an account' do
    user = User.new(account: nil)
    user.valid?
    expect(user.errors[:mail]).to include("can't be blank")
  end

  # ...

end
```

rspecコマンドによってスペックを再度検証し、対象がスペックを満たすことを確認する。

```bash
$ ./bin/rspec

#> User
#>   is valid with a mail and account
#>   is invalid without a mail
#>   is invalid without an account
#> 
#> Finished in 0.5441 seconds (files took 2.86 seconds to load)
#> 3 examples, 0 failures
```

結果として問題がなければスペックを満たし、テスト合格と判断できる。
結果に問題があれば特定した上で対象あるいはスペックを書き直し、この流れを繰り返す。



検査の書き方
------------------------------------------------------------

`it()`においては、テスト対象がどのように振る舞うことを__期待__するのかを`except`を使って記述していく。
exceptは従来のテストにおけるアサーションに類似する概念で、以下のようにメソッドをつなげて英文形式で記述できる。

```rb
# 評価が真であることを期待する
expect([評価の対象]).to [評価の方法(matcher)]

# 評価が偽であることを期待する
expect([評価の対象]).not_to [評価の方法(matcher)]
```


### expect

`expect`(=期待、予測)は、検査の対象を指定する。
expectにはActiveRecordを含むオブジェクトのほか、ブロックを渡してその処理の結果を検証することもできる。


例                              | 意味
---                             | ---
`expect(model)`                 | ActiveRecordオブジェクトを検査する
`expect(asigns(variable))`      | コントローラスペックにおいて、指定されたインスタンス変数の値を検査する
`expect(response)`              | `Rack::Test`系のメソッドにおける`get`や`post`の結果を検査する
`expect { ... }`                | ブロック内で実行された処理による影響を検査する



### matcher

matcherは、`expect` で指定した検査の対象に対して期待する振る舞い(条件)を指定する。

例                          | 意味
---                         | ---
`be_valid`                  | 対象が.valid?であること
`include(pattern)`          | 対象の文字がpatternを含むこと
`eq(value)`                 | 対象がvalueと等価であること
`match(pattern)`            | 対象が(正規表現の)パターンに合致すること
`match_array(ary)`          | 対象が配列に合致すること
`have_content(txt)`         | 対象が指定の文字を含むこと
`be_true`                   | 対象が真
`be_false`                  | 対象が偽
`redirect_to(url)`          | リダイレクトされるか
`render_template(temp)`     | テンプレートがレンダリングされるか
`change(User, :count).by(1)`| DBの件数の変化

デフォルトのmatcherの一覧は[公式のリファレンス](https://relishapp.com/rspec/rspec-expectations/docs/built-in-matchers)を参照のこと。
また後述するようにmatcherを自作したり、サードパーティのものを使うこともできる。



スペックの構成と管理
------------------------------------------------------------

### 記述を階層化する(describe/context)

`describe`の引数には、テストの対象となるクラス(モデルやコントローラ)か、テスト項目を記した文字列を渡せる。
それぞれのスペックのトップレベルのdescribeには通常、対象クラスを設定する。

`describe`はネストすることができる。
振る舞いをグループ化して可読性を上げたり、それぞれ別のフック(後述)を設定したりできる。

また、describeと同機能の`context`メソッドを使うこともできる。
両者はどちらをつかってもいいが、おおむね、

- describeは__対象__(〜について) を分類する
- contextはその__状態__(〜な場合) を分類する


```rb
describe User do
  describe 'validation' do
    context 'valid' do
      # ...
    end
    context 'invalid' do
      # ...
    end
  end

  describe 'authentication' do
    # ...
  end
end
```

### 主語を明示する形式で記述する(subject/specify/should)

`specify`は`it`と等価で、振る舞いの検査(example)を意味する。
`subject`によってテストの対象を明確にすることで、`expect`を省略し`should(_not)`を用いた記述ができる。

```rb
describe User do
  subject { create(:user, mail: nil) }
  specify { should_not be_valid }
end
```


### フィーチャーを記述する(feature/scenario/given)

フィーチャースペック(機能単位の結合テスト)を記述する場合、
describe/it/let に替えて `feature` / `scenario` / `given` と書ける。

```rb
# describe '...' type: :feature に同じ
feature 'Session Control' do

  # let に同じ
  given :user do
  end

  # it に同じ
  scenario 'signup' do
  end

  scenario 'login' do
  end

  # ...
end
```

### タグをつける

examplesに任意のフラグ(タグ)を付与し、実行時にタグを選択して実行できる。

```rb
# タグmustを付与
it '...', must: true do
    # ...
end
```

```bash
# タグを特定して実行
$ ./bin/rspec --tag must
```


### 検査を一時的に無効化する

`skip`を使うとその検査を一時的にとばせる。

```rb
it '...' do
  skip 'スキップする理由や説明'
end
```


DRY化
------------------------------------------------------------

### フックを使う(before/after)

フックを使うと、振る舞いごとの検査の前後のタイミングで任意の処理を実行できる。
フック内で定義したインスタンス変数は、そのスコープ内での検査で利用できる。

```rb
describe User do
  before :each do
    # 振る舞いの検査ごとの実行前に毎回処理される
    @foo = ...
    @bar = ...
  end

  after :each do
    # 振る舞いの検査ごとの実行後に毎回処理される
  end

  it 'is this example' do
    # 定義したインスタンス変数を利用できる
    expect(@foo)
  end
end
```

before/afterフックはオプションによってそのタイミングを指定できる。

オプション  | タイミング
---         | ---
`:each`     | 各exampleごとに実行(デフォルト)
`:example`  | `:each`と同じ
`:all`      | describe/contextごとに実行
`:context`  | `:all`と同じ
`:suite`    | 一度だけ実行


### 検査項目を共通化する(shared_examples / it_behaves_like)

複数のdescribe/contextに共通して同じ項目を検査したい場合、`shared_example`と`it_behaves_like`を使う。
異なるテストデータに対して同じ項目を検査したい場合などに使う。


```rb
# 共通する検査項目を定義する
shared_examples 'some behavior' do

  it '...' do
  end

  # ...
end

describe 'Foo' do
  before :each do
    @user = build(:foo)
  end

  # 共通項目を検査する
  it_behaves_like 'some behavior'
end

describe 'Bar' do
  before :each do
    @user = build(:bar)
  end

  # 共通項目を検査する
  it_behaves_like 'some behavior'
end
```


### マクロを定義する(support)

スペック内で繰り返し記述されうる処理をマクロ(ヘルパ)によって共通化できる。
マクロは通常`spec/support/`以下に配置する。

```rb:spec/support/foo_macros.rb
module FooMacros
  def some_operation()
    # ...
  end
end
```

マクロは`rails_helper.rb`上でインクルードすることによって、スペックで利用可能になる。

```diff:spec/rails_helper.rb
RSpec.configure do |config|
+  config.include FooMacros
end
```

```diff:spec/models/user_spec.rb
describe User
  before :each do
    some_operation()    # 使える
  end
end
```

#### matcherを自作する

振る舞いの検査のためのmatcherを自作して使うこともできる。
通常は`spec/support/matchers/`に配置する。


```rb:spec/support/matchers/some_match.rb
RSpec::Matchers.define :some_match do |expected|

  # 検証方法を実装する
  match do |actual|
    # ...
  end

  # 検証失敗時のメッセージを設定する
  failure_message do |actual|
    '...'
  end
  failure_message_when_negated do |actual|
    '...'
  end
  description do
    '...'
  end
end
```


テストデータ
------------------------------------------------------------

### テストデータを定義する

テストデータの準備・構築はRSpec自体の機能からは切り離されているので、任意の方法を選択できる。
たとえば、

- ActiveRecordモデルクラスを直接作成 / DB保存する
- fixturesを使う
- FactoryGirlを使う

など。

#### ActiveRecordを直接操作する

スペック内ではRailsのモデルが制限なく利用できる。
テストクラスをnewしたり、DBに保存したりしてexamplesの記述に使える。

```rb
# 重複するデータを先にDBに保存しておき、バリデーションエラーを期待する
User.create(mail: 'foo@example.com')
user = User.new(mail: 'foo@example.com')
expect(user).not_to be_valid
```

#### FactoryGirlを使う

導入時に設定したディレクトリ(今回の例では`/spec/factories`)にファクトリを配置することで、スペック内で利用できる。

```rb:spec/factories/users.rb
define do
  factory :user do
    mail: 'foo@example.com'
    account: 'foo'
  end
end
```

```rb:spec/models/user_spec.rb
# 導入時にFactoryGirl::Syntax::Methodsをincludeしていない場合は、
# FactoryGirl.buildと記述する必要がある
user = build(:user)
expect(user).not_to be_valid
```


### 共通するテストデータを保持する(let)

共通のテストデータを準備する方法を以下:

- beforeフックで作成し、インスタンス変数を通じて各exampleに渡す
- `let`あるいは`let!`によって変数定義する


`let`によって定義したテストデータは__遅延評価__される。
また`let!` の場合は各exampleの実行直前に、変数が__必ず評価される__。

```rb
describe User do
  let :alice do
    create(:user, name: 'Alice')
  end

  it '...' do
    # ローカル変数のようにアクセスできる
    expect(alice)...
  end

end
```


通信をテストする
------------------------------------------------------------

### リクエストを投げる

Rails標準のテスト環境と同様に、Rackが提供するテスト用メソッド(`Rack::Test`)を利用できる。
リクエストをシミュレートし、コントローラやルーティングを検証する際に使う。
get/post/patch/delete...

```rb
# 結果はローカル変数responseに格納される
post :create user: attributes_for(:user)

# 後述するexpect()による検査に利用できる
expect(response).to redirect_to some_url
```


### 画面遷移とUI操作を検査する

フィーチャースペックにおいて複数画面間の遷移や、フロント(UI)の操作を含めた動作を確認したい場合は、
RSpecに[capybara](https://github.com/teamcapybara/capybara) や [cucumber](https://github.com/cucumber/cucumber)などのE2E用テストフレームワークを組み合わせて使う。


参考
------------------------------------------------------------
- [RSpec](http://rspec.info/)

- [Relish - RSpec](https://relishapp.com/rspec/) ← これが公式のリファレンスらしい
    - [Built in matchers](https://relishapp.com/rspec/rspec-expectations/docs/built-in-matchers)

- [Github - rspec/rspec-rails](https://github.com/rspec/rspec-rails)
- [Github - teamcapybara/capybara](https://github.com/teamcapybara/capybara)
- [Github - cucumber/cucumber](https://github.com/cucumber/cucumber)
- [Github - thoughtbot/shoulda](https://github.com/thoughtbot/shoulda)

- [(書籍)leanpub - Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp/)