RSpec について勉強したまとめ

これは何?


会社で RSpec を使った、BDD を行っているのて理解の為にまとめてみました

RSpecってんなんぞ??


一言でいうと、BDD の為の DSL

日本語でおk

DSL


ドメイン固有言語
やりたい事に特化した言語
今回の場合は、BDD に特化した言語(実際には、gem を使った Ruby の拡張)

BDD


振舞駆動開発(behavior driven development)
プログラムの振舞(≒要求仕様)を先に書く開発手法
っていうと普通だけど、その振舞がテストコードにもなる
そのための DSLRSpec

BDDの歴史


XPのテスト駆動開発(TDD)

テストにより、逆に仕様が明確化

テストってある意味要求仕様と同一だよね

自然言語で書かれれば、いろいろ便利だよね

DSLの開発(RSpec等)

BDD

BDDだと何が得?


TDDより、テストコードが可読性に優れている(自然言語(英語)に近い)事

そんだけ?


どうやらそんだけらしい
ただし、可読性が優れる事により、

  • テストの漏れが減る
  • 要求仕様をプログラマ以外も読める
  • 仕様書からテストコードの移行が容易

等、利点は大きいと、言われてる

具体的にはどんな感じなの?


こんな感じ


テストされるコード

class TestClass 
	def addition(a,b)
		return a+b
	end
	
	def evenNumber?(a)
		return a.to_i % 2 == 0
	end
end 


Specのコード

# describeメソッドにて、ひとまとまりの振る舞いを定義
describe TestClass, "が、行う処理" do 
	before(:all) do
		# 事前処理(describe毎に1回呼ばれる)
	end

	before do
		# 事前処理(example毎に1回呼ばれる)
		@testClass = TestClass.new
	end

	# example(実行可能なサンプル)
	# exampleの中身(〜 should … の事をエクスペクテーションと呼ぶ)
	it "は、足し算が正しく行われる事" do
		@testClass.addition(1,2).should == 3
	end
	
	it "は、偶数奇数を、判別できること" do
		@testClass.evenNumber?(2).should be_true
		@testClass.evenNumber?(1).should be_false
	end

	after do
		# 事後処理(example毎に1回呼ばれる)
	end
	
	after(:all) do
		# 事後処理(describe毎に1回呼ばれる)
	end
end 


実行結果(正常)

# fsで、レポート形式で出力
# c で色がつく
spec -fs specDemo.rb

TestClass が、行う処理
- は、足し算が正しく行われる事
- は、偶数奇数を、判別できること

Finished in 0.447497 seconds

2 examples, 0 failures

describe


ひとまとまりの振舞を記載
書き方は以下の3通り

  • describe "説明"
  • describe 対象のクラス, "説明"
  • describe 対象のクラス

describeのブロック内に具体的なexampleを書く

example


振舞における個々の項目を記載
it メソッドにて定義
メソッド名が it であることで、以下のように自然文(英語)に近く、可読性が高い文が書ける

it "should 〜(は、〜であること)" do … end

エクスペクテーション


example のブロックに記述される、プログラムに期待する振舞
should メソッドにて定義
should メソッドは、Object クラスに追加されるので、あらゆるインスタンスで使用可能
should メソッドの引数にはマッチャと呼ばれる、期待する振舞を表すオブジェクトを渡す

マッチャの例
# 演算子マッチャ
(1+2).should == 2

# 述語マッチャ
# be_xxx ならば、対象のインスタンスの xxx? メソッドが呼ばれて、trueならばパス
# 下記の場合、test.empty? が呼ばれる
test.should be_empty

# 例外
# 例外が発生する箇所を、lamdaでくくる必要がある
lamda{
      test.test_method()
}.should raise_error(StandardError, 'errorMsg')

# その他
test.should be_nil
test.should be_true
test.should_not be_false


この辺も参考になりそう(ちょっと古いらしいけど)

特殊なエクスペクテーション

violated メソッド


exampleで使用
必ずテストが失敗する
テストが未実装である事を表明

it "は、足し算が正しく行われる事" do
	violated "まだテスト未実装"
end
pending メソッド


現在は例外が発生するけども、それはペンディングだよ。という表明で使用
例外が発生する部分をブロックにする
ブロックにした場合は、例外が発生しないと、例外が発生する(ややこしい…)

it "は、足し算が正しく行われる事" do
	pending("ここは保留") do
		@testClass.addition(1,2).should == 3
	end
end

it "なにか理由" do
	# do をつけないと、以降が全て pending となる
	pending("以降保留") doend


この辺も参考
RSpecの violated と pending - ひげろぐ

モックとスタブ

・スタブは、テスト時の呼び出しに対して、あらかじめ用意された結果を返す。
 通常、テスト用にプログラムされたところ以外には応答しない。
 スタブは呼び出しの情報を記録することもある。
 例えば、Eメールゲートウェイスタブは「送られた」メッセージを記録するような場合だ。
 単に「送られた」メールの数を記録する場合もあるだろう。

・モックは、エクスペクテーションが事前にプログラムされたものである。
 エクスペクテーションとは、受信する一連の呼び出しの仕様を表わしたものである。
 期待されない呼び出しが行なわれた場合は例外をスローする。
 また、テスト実行後の検証 (verification)で、期待された呼び出しがすべてきちんと行われたかどうかを確認する。

Martin Fowler's Bliki in Japanese - テストダブル

  • スタブは、固定値を返却する
  • モックは、振舞が定義されている

らしい。いまいち分かりきれていない

使い道・利点

  • 個々のクラスやオブジェクトを完全に分離して、テストできる
  • そのようにするため、各クラスの依存性が下がる
  • 各クラスのAPIがきれいになる


スタブメソッドと、shoud_receive について勘違いがあるので、後日修正します

RSpecでの実例


具体的にはこんな感じ

describe "MockSample" do
  before do
    # mockの定義
    @mockTest = mock('MockTest')
  end

  it 'は、mockが正しく動作すること' do
    # shoud_receive でスタブメソッドを定義
    # with で、メソッドの引数を定義
    #  引数が固定値文字列ならば、それを指定
    # and_returnで戻り値を定義
    @mockTest.should_receive(:test).with(:test).and_return('OK')
    # 型の固定(an_instance_of)
    # and_returnがブロックの場合、その結果を返す
    @mockTest.should_receive(:test2).with(an_instance_of(String)).and_return{|a| a}
    # and_raiseで例外をreturn
    # any_number_of_timesで、何度も呼び出し可能(回数を指定しないと、1回しか呼べない)
    @mockTest.should_receive(:test3).any_number_of_times.and_raise("mockError")
    
    @mockTest.test(:test).should == 'OK'
    @mockTest.test2('testtest').should == 'testtest'
    lambda{
      @mockTest.test3()
    }.should raise_error(StandardError, 'mockError')
    lambda{
      @mockTest.test3()
    }.should raise_error(StandardError, 'mockError')
  end
end