250
253

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RSpecチートシート

Last updated at Posted at 2017-07-17

前回: FactoryGirlチートシート

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

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

概要

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

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

導入

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

Gemfile
# 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ファイルも同時に作成されるように設定変更する。

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を使う場合に名前空間を省略できるように設定する。

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__と呼ばれる)

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

$ ./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

振る舞いを実装する

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

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コマンドによってスペックを再度検証し、対象がスペックを満たすことを確認する。

$ ./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系のメソッドにおけるgetpostの結果を検査する
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)

specifyitと等価で、振る舞いの検査(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_exampleit_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/以下に配置する。

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

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

spec/rails_helper.rb
RSpec.configure do |config|
+  config.include FooMacros
end
spec/models/user_spec.rb
describe User
  before :each do
    some_operation()    # 使える
  end
end

matcherを自作する

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

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の記述に使える。

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

FactoryGirlを使う

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

spec/factories/users.rb
define do
  factory :user do
    mail: 'foo@example.com'
    account: 'foo'
  end
end
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の実行直前に、変数が__必ず評価される__。

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にcapybaracucumberなどのE2E用テストフレームワークを組み合わせて使う。

参考

250
253
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
250
253

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?