FactoryGirlに続いてRSpecの使い方を調べてみました。
当文書は単にツールとしてのRSpecの使い方のまとめです。
TDD/BDDの概念やワークフロー、あるいは良いテストを書くための指針等にはふれていません。
概要
RSpec = Rubyアプリケーションのテストのためのフレームワーク
- オープンソース / MITライセンス
- ビヘイビア駆動開発(BDD)をサポートする
- Spec(ification) = 仕様 = テストコード。コードの振る舞いを試験するためのテストと仕様を同じものととらえている
- 2017/07現在のバージョンは3.6系
導入
Railsの場合はgem rspec-rails
をインストールする。
# development環境でも利用する(し得る)点に注意
group :development, :test do
gem 'factory_girl_rails'
gem 'rspec-rails'
# ...
end
$ bundle install
RSpecの最低限必要なファイル/ディレクトリ構成をRailsにインストールする。
$ 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
でもいい)
$ 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ファイルも同時に作成されるように設定変更する。
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を使う場合に名前空間を省略できるように設定する。
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__と呼ばれる)
# 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コマンドによってスペックを検証すると、具体的な振る舞いが実装されていないため保留状態となっていることが示される。
$ ./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
振る舞いを実装する
列挙した振る舞いに対してブロックを渡し、振る舞いの詳細を実装していく。
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コマンドによってスペックを再度検証し、対象がスペックを満たすことを確認する。
$ ./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は従来のテストにおけるアサーションに類似する概念で、以下のようにメソッドをつなげて英文形式で記述できる。
# 評価が真であることを期待する
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の一覧は公式のリファレンスを参照のこと。
また後述するようにmatcherを自作したり、サードパーティのものを使うこともできる。
スペックの構成と管理
記述を階層化する(describe/context)
describe
の引数には、テストの対象となるクラス(モデルやコントローラ)か、テスト項目を記した文字列を渡せる。
それぞれのスペックのトップレベルのdescribeには通常、対象クラスを設定する。
describe
はネストすることができる。
振る舞いをグループ化して可読性を上げたり、それぞれ別のフック(後述)を設定したりできる。
また、describeと同機能のcontext
メソッドを使うこともできる。
両者はどちらをつかってもいいが、おおむね、
- describeは__対象__(〜について) を分類する
- contextはその__状態__(〜な場合) を分類する
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)
を用いた記述ができる。
describe User do
subject { create(:user, mail: nil) }
specify { should_not be_valid }
end
フィーチャーを記述する(feature/scenario/given)
フィーチャースペック(機能単位の結合テスト)を記述する場合、
describe/it/let に替えて feature
/ scenario
/ given
と書ける。
# describe '...' type: :feature に同じ
feature 'Session Control' do
# let に同じ
given :user do
end
# it に同じ
scenario 'signup' do
end
scenario 'login' do
end
# ...
end
タグをつける
examplesに任意のフラグ(タグ)を付与し、実行時にタグを選択して実行できる。
# タグmustを付与
it '...', must: true do
# ...
end
# タグを特定して実行
$ ./bin/rspec --tag must
検査を一時的に無効化する
skip
を使うとその検査を一時的にとばせる。
it '...' do
skip 'スキップする理由や説明'
end
DRY化
フックを使う(before/after)
フックを使うと、振る舞いごとの検査の前後のタイミングで任意の処理を実行できる。
フック内で定義したインスタンス変数は、そのスコープ内での検査で利用できる。
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
を使う。
異なるテストデータに対して同じ項目を検査したい場合などに使う。
# 共通する検査項目を定義する
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/
以下に配置する。
module FooMacros
def some_operation()
# ...
end
end
マクロはrails_helper.rb
上でインクルードすることによって、スペックで利用可能になる。
RSpec.configure do |config|
+ config.include FooMacros
end
describe User
before :each do
some_operation() # 使える
end
end
matcherを自作する
振る舞いの検査のためのmatcherを自作して使うこともできる。
通常はspec/support/matchers/
に配置する。
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の記述に使える。
# 重複するデータを先にDBに保存しておき、バリデーションエラーを期待する
User.create(mail: 'foo@example.com')
user = User.new(mail: 'foo@example.com')
expect(user).not_to be_valid
FactoryGirlを使う
導入時に設定したディレクトリ(今回の例では/spec/factories
)にファクトリを配置することで、スペック内で利用できる。
define do
factory :user do
mail: 'foo@example.com'
account: 'foo'
end
end
# 導入時にFactoryGirl::Syntax::Methodsをincludeしていない場合は、
# FactoryGirl.buildと記述する必要がある
user = build(:user)
expect(user).not_to be_valid
共通するテストデータを保持する(let)
共通のテストデータを準備する方法を以下:
- beforeフックで作成し、インスタンス変数を通じて各exampleに渡す
-
let
あるいはlet!
によって変数定義する
let
によって定義したテストデータは__遅延評価__される。
またlet!
の場合は各exampleの実行直前に、変数が__必ず評価される__。
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...
# 結果はローカル変数responseに格納される
post :create user: attributes_for(:user)
# 後述するexpect()による検査に利用できる
expect(response).to redirect_to some_url
画面遷移とUI操作を検査する
フィーチャースペックにおいて複数画面間の遷移や、フロント(UI)の操作を含めた動作を確認したい場合は、
RSpecにcapybara や cucumberなどのE2E用テストフレームワークを組み合わせて使う。