kozzy’s blog

Ruby, Python, PHP, React.js あたりで学んだこと、記事、読んだ本のまとめなどを垂れ流しする場の予定です

RSpec によるテスト駆動開発はじめのいっぽ

目次

この記事は?

  • Rubyテスト駆動開発を行うためのメジャーな Gem である Rspec についての記事を参考にしつつ、まとめている。

Why

  • 業務上、Rspec を使って、テストを実装する必要があったので調査した記事をとりまとめて備忘録としたい
  • テストの実装に苦手意識があるので、克服したい

基本的な使い方

install と init ファイルの生成

rspec を動かす上で必要な、gem install と spec_helper,rb ファイルなどの生成を行う。

$ gem install rspec
$ rspec --init
  create   spec/spec_helper.rb
  create   .rspec

サンプルコード

Human クラス のテストを行うコードは以下のように書く。

lib/human.rb

class Human
  attr_accessor :name, :hands

  def initialize(name="Kozzy")
    @name = name
    @hands = 2
  end

  def alived?
    true
  end
end

spec/lib/human_spec.rb

require "spec_helper"
require "human"

describe Human do
  it "named 'Koji'" do
    human = Human.new
    expect(human.name).to eq 'Kozzy'
  end

  it "has hands" do
    human = Human.new
    expect(human.fangs).to eq 2
  end

  it "is alived" do
    human = Human.new
    expect(human).to be_alived
  end
end
$ rspec spec/lib/human_spec.rb

qiita.com

メモ

describe, context, subject, letなどの書き分け

テスト名の宣言

  • describe: テストグループ ( クラス名, メソッド名, )
  • context: テスト条件 ( 変数の違いや前提条件の違い )
  • it: テスト結果

変数宣言について

  • subject: テスト対象 = expectの引数になるもの
  • let: expectの引数になり得ないもの
  • @hoge: 使わない

kei-p3.hatenablog.com

before と let の実行タイミング

beforelet!

  • どちらも、ブロックが定義された時に実行される。

let

  • ちなみにletは、変数が初めて使用された時だけ遅延評価され、specテストが終わるまでキャッシュとして使える。

qiita.com

before(:each) を使おう

before(:all)before(:each) の違い

  • before(:all)context / describe ブロックが始まる時に一度だけ実行される
  • before(:each) は各スペック内のitの前に実行される

通常、各スペックは独立してテストしたい。 でも、 before(:all) を使うとそれができなくなる。

qiita.com

Matcher 一覧

同じ値になるか確認

expect(actual).to eq(expected)  # passes if actual == expected
expect(actual).to eql(expected) # passes if actual.eql?(expected)

同一のオブジェクトかを確認

expect(actual).to be(expected)    # passes if actual.equal?(expected)
expect(actual).to equal(expected) # passes if actual.equal?(expected)

大小を確認

expect(actual).to be >  expected
expect(actual).to be >= expected
expect(actual).to be <= expected
expect(actual).to be <  expected
expect(actual).to be_within(delta).of(expected)

正規表現がマッチするか確認

expect(actual).to match(/expression/)

同一クラスか確認

expect(actual).to be_an_instance_of(expected) # passes if actual.class == expected
expect(actual).to be_a(expected)              # passes if actual.is_a?(expected)
expect(actual).to be_an(expected)             # an alias for be_a
expect(actual).to be_a_kind_of(expected)      # another alias

True or False

expect(actual).to be_truthy # passes if actual is truthy (not nil or false)
expect(actual).to be true   # passes if actual == true
expect(actual).to be_falsy  # passes if actual is falsy (nil or false)
expect(actual).to be false  # passes if actual == false
expect(actual).to be_nil    # passes if actual is nil

例外処理

expect { do_exception }.to raise_error
expect { do_exception }.to raise_error(ErrorClass)
expect { do_exception }.to raise_error("message")
expect { do_exception }.to raise_error(ErrorClass, "message")

例外が吐かれる事を確認

expect { do_exception }.to throw_symbol
expect { do_exception }.to throw_symbol(:symbol)
expect { do_exception }.to throw_symbol(:symbol, 'value')

yieldの確認

expect { |b| 5.tap(&b) }.to yield_control # passes regardless of yielded args
expect { |b| yield_if_true(true, &b) }.to yield_with_no_args # passes only if no args are yielded
expect { |b| 5.tap(&b) }.to yield_with_args(5)
expect { |b| 5.tap(&b) }.to yield_with_args(Fixnum)
expect { |b| "a string".tap(&b) }.to yield_with_args(/str/)
expect { |b| [1, 2, 3].each(&b) }.to yield_successive_args(1, 2, 3)
expect { |b| { :a => 1, :stuck_out_tongue: => 2 }.each(&b) }.to yield_successive_args([:a, 1], [:b, 2])

値を持っているか確認

expect(actual).to be_xxx         # passes if actual.xxx?
expect(actual).to have_xxx(:arg) # passes if actual.has_xxx?(:arg)

範囲に含まれている事を確認

expect(1..10).to cover(3)

コレクションを確認

expect(actual).to include(expected)
expect(actual).to start_with(expected)
expect(actual).to end_with(expected)
expect(actual).to contain_exactly(individual, items)
# ...which is the same as:
expect(actual).to match_array(expected_array)

github.com

Rspec のベストプラクティス

Contextsを使う

  • Contextsはテストを明らかにし、まとめる素晴らしい方法です。 長い目で見ると、この方法はテストを読みやすくします。

説明を短く

  • specの説明は 40文字を超えない ようにしましょう。

単一条件テスト

  • 独立したユニットでは、 各例はただ一つの振る舞いだけテストするのが望ましい です。

Subjectを使う

  • もしも、同じsubjectに対して複数のテストをしていたら、subject{}を使ってDRYしましょう。

letとlet!を使う

  • 変数に値を入れる必要がある時はbeforeブロックの代わりにletを使いましょう。

Shared Examples

  • テストを作るのは素晴らしいです。毎日少しづつ自信がつきます。が、結局色んな所にコードの重複が発生します。shared exampleを使ってテストをDRYしましょう。

www.betterspecs.org

FactoryGirl について

  • FactoryGirlによって、テストデータを定義・生成することができる

継承する

ファクトリをネスト定義することで、親の内容を継承できる。

factory.rb

FactoryGirl.define do
  factory :user do
    name "Kozzy"
    email  "kozzy@hoge.com"
    admin false
  end
end

_spec.rb

# インスタンスを生成
user = create(:user)

qiita.com

qiita.com

最新のものは Factory_bot と名を変えているらしい

qiita.com

shared_example について

  • shared_example を用いると、共通部分の括りだしを行うことができる
shared_examples_for 'Some Example' do
  it 'do something' do
    # Some Tests here
  end
end

describe '#index' do
  context 'case 1' do
    it_behaves_like 'Some Example'
  end
  context 'case 2' do
    it_behaves_like 'Some Example'
  end
end

パラメータ渡しもできる

RSpec.shared_examples "some example" do |parameter|
  let(:something) { parameter }
  it "uses the given parameter" do
    expect(something).to eq(parameter)
  end
end

RSpec.describe SomeClass do
  include_examples "some example", "parameter1"
  include_examples "some example", "parameter2"
end

定義した example を呼び出す方法

include_examples "name"      # include the examples in the current context
it_behaves_like "name"       # include the examples in a nested context
it_should_behave_like "name" # include the examples in a nested context
matching metadata            # include the examples in the current context

samurai.ataglance.jp

relishapp.com

書籍

ちょっと前に Everyday Rails を買っていた。体系的に学び直すにはやはりこれかな。

leanpub.com