テスト

ここではRubyで記述されたコードに対するテスト方法の概要について説明します。Rubyには、ユニットテストをしやすくするフレームワーク(ライブラリ)が提供されています。通常は、個々のモジュールやメソッドなど小さな単位で十分なユニットテストを行って検証し、結合テストへと進みます。

提供されるフレームワークは、「テスト駆動開発(Test Driven Development:TDD)」や「振舞駆動開発(Behaviour Driven Development:BDD)」という思想がベースになっています。テスト駆動開発とは、プログラム開発手法の一つで、プログラムに必要な各機能について、最初にテストコードを書きそれが失敗することを確認し(テストファースト)、そのテストが成功するように必要最低限の実装を行った後、プログラムの振る舞いを変えないようにコードを洗練(リファクタリング)していく方法です。これによって、プロダクトコードが正しく動作することが検証可能になるばかりでなく、進捗の明確化、仕様変更への対応の容易さなどのメリットをもたらします。一方、振舞駆動開発は、テストコードを書いてそれを実現するように実装する点では同じですが、プログラムに期待される振る舞い(要求仕様)を、自然言語に近い形でテストコードとして記述することが特徴です。したがって、要求仕様とテストは近いものとなります。

代表的なフレームワークとしては、Rubyに標準で添付されているTest::Unit(日本語)や、その拡張であるShoulda、振舞駆動開発に特化したRSpecがあります。Ruby on Railsでのアプリケーション開発でも、これらのフレームワークを用いたテストができます。

以下ではRSpecを取り上げて、インストール方法や使い方を紹介します。

 

RSpecとは

RSpecとはプログラムの振る舞いを記述するためのドメイン特化言語(DomainSpecific Language:DSL)を提供するフレームワークです。ここで振る舞いとはクラスやメソッドの挙動のことを指します。またドメイン特化言語とは、特定の目的のために設計された言語であり、ここではプログラムの振る舞いを記述することに特化したものになります。
典型的なRSpecによるテストコードを以下に示します。基本的にはテスト対象の状態を設定し、与えられた入力によって期待通りの出力が得られるかを検証するように実装します。これを実行すると、describeとcontextやitで指定した引数の文字列がそのまま出力され、それが仕様書のように表示されます。

 

describe 'テストする対象(例:Stack)' do
context 'テストの状況(例:新しく生成したとき)' do
#テストを実行する前処理
before do
@stack = Stack.new
end

it 'テストの説明(例:スタックが空であること)' do
#期待する出力との照合
@stack.should be_empty
end
end
end

 

RSpecのインストール

コンソールからgem install rspecコマンドで最新のRSpecをインストールします。 

C:\Users\RA>gem install rspec
Fetching: rspec-core-2.9.0.gem (100%)
Fetching: diff-lcs-1.1.3.gem (100%)
Fetching: rspec-expectations-2.9.1.gem (100%)
Fetching: rspec-mocks-2.9.0.gem (100%)
Fetching: rspec-2.9.0.gem (100%)
Successfully installed rspec-core-2.9.0
Successfully installed diff-lcs-1.1.3
Successfully installed rspec-expectations-2.9.1
Successfully installed rspec-mocks-2.9.0
Successfully installed rspec-2.9.0
5 gems installed
(途中略)

 

RSpecが無事にインストールされていることを確認します。

C:\Users\RA>rspec -v
2.9.0

 

またRSpecでは、実行結果を色付けで表示することが可能です。Windowsではコマンドプロンプトで色付け表示ができないため、ansiconをインストールしておきます。

ansiconのダウンロードサイトに進んで、最新版のansiXXX.zip(XXXはバージョン番号)をクリックしてダウンロードします。

ansiconのダウンロードサイト

 

zipファイルを任意のフォルダに解凍し、コマンドプロンプトで、32bit OSのときは(解凍先パス)\ansiXXX\x86フォルダ、64bit OSなら(解凍先パス)\ansiXXX\x64フォルダに移動します(XXXはバージョン番号)。そこでansicon -iコマンドを実行するとインストールが実行されます(ansiconがAutoRunレジストリに設定されます)。ここで解凍したファイルは削除しないでください。

c:\ruby\ansi151\x86>ansicon -i

 

以上でインストールは終了です。

 

RSpecの基本的な実行方法

以降、Ruby1.9.3、RSpec2.9.0の環境を前提として、説明します。
RSpecを実行するには、プロダクトコードに対するテストコードを準備します。RSpecのテストコードはスペックファイルと呼ばれ、一般的にはファイル名を_spec.rbで終わるようにします。
次のようなスペックファイルを準備し(要求仕様をテストコードとして記述し)、UTF-8で保存します。ここではStackオブジェクト@stackを生成し、生成した直後なので@stack.empty?がtrueか、@stack.sizeが0かの二つを確認するテストを書いています。

 

<stack_spec.rb>

# coding: utf-8

describe "Stack" do
context "新しく生成したとき" do
before do
@stack = Stack.new
end

it "スタックが空であること" do
@stack.should be_empty
end

it "サイズが0であること" do
@stack.size.should == 0
end
end
end

 

まだプロダクトコードは実装していませんが、このままの状態で実行します。実行するには、コマンドプロンプトを立ち上げて、上記スペックファイルを保存したディレクトリに移動し、「rspec (スペックファイル名)」コマンドで実行します。

C:\ruby\rspec_test>rspec stack_spec.rb
FF

Failures:

1) Stack 新しく生成したとき スタックが空であること
Failure/Error: @stack = Stack.new
NameError:
uninitialized constant Stack
# ./stack_spec.rb:6:in `block (3 levels) in '

2) Stack 新しく生成したとき サイズが0であること
Failure/Error: @stack = Stack.new
NameError:
uninitialized constant Stack
# ./stack_spec.rb:6:in `block (3 levels) in '

Finished in 0.01001 seconds
2 examples, 2 failures

Failed examples:

rspec ./stack_spec.rb:9 # Stack 新しく生成したとき スタックが空であること
rspec ./stack_spec.rb:13 # Stack 新しく生成したとき サイズが0であること

C:\ruby\rspec_test>

 

コマンド直後の「FF」という表記は、2つのテスト(これをexampleとよびます)を実行し、2つとも失敗(Failure)であることを示しています。その後のFailures以降で、失敗したテストの内容と結果が表示されています。
実行結果に色付けをするには-cオプション(または--color--colourオプション)を入れます。色付け後はテストに失敗すると、赤色で表示されます。

C:\ruby\rspec_test>rspec -c stack_spec.rb
FF

Failures:

1) Stack 新しく生成したとき スタックが空であること
Failure/Error: @stack = Stack.new
NameError:
uninitialized constant Stack
# ./stack_spec.rb:6:in `block (3 levels) in '

2) Stack 新しく生成したとき サイズが0であること
Failure/Error: @stack = Stack.new
NameError:
uninitialized constant Stack
# ./stack_spec.rb:6:in `block (3 levels) in '

Finished in 0.01001 seconds
2 examples, 2 failures

Failed examples:

rspec ./stack_spec.rb:9 # Stack 新しく生成したとき スタックが空であること
rspec ./stack_spec.rb:13 # Stack 新しく生成したとき サイズが0であること

C:\ruby\rspec_test>

スペックファイルのあるディレクトリに「.rspec」というファイル名でファイルを作成し、内容に「--color」と書いておけば、オプション指定がなくても常に色づけされて表示されます。他にも常に利用するオプションがあれば、このファイルに書いておくと便利です。

 

さて、失敗したテストを合格させるために、プロダクトコードを実装します。以下のファイルを、スペックファイルと同じ場所に保存します。

<stack.rb>

class Stack
def initialize
@stack = []
end

def empty?
@stack.empty?
end

def size
@stack.size
end
end

スペックファイルにstack.rbを読み込むようにするため、requireを追記します。

<stack_spec.rb> 

# coding: utf-8
require './stack' #追記部分

describe "Stack" do
・・・

 

これでテストを-cオプションを付けて実行します。

C:\ruby\rspec_test>rspec -c stack_spec.rb
..

Finished in 0.05007 seconds
2 examples, 0 failures

C:\ruby\rspec_test>

 

コマンド直後の2つのドット(.)は、2つのテスト(example)が成功したことを意味しています。つまり2つのテストとも、期待する結果と一致したことを示しています。成功したテストは緑色で表示されます。

またコマンドrspecに-fdオプションを指定すると、実行結果の内容が仕様書のように出力されます。

C:\ruby\rspec_test>rspec -fd stack_spec.rb

Stack
新しく生成したとき
スタックが空であること
サイズが0であること

Finished in 0.0583 seconds
2 examples, 0 failures

C:\ruby\rspec_test>

 

また--profileオプションを付けると各テストにかかった時間が表示されます。他にも様々なオプションがありますが、詳しくはrspec --helpで参照してください。

 

スペックファイルの書き方

RSpecのテストコードの構造や書き方について説明します。ここでは全体の概要(RSpecで何ができるか)について示しますので、実際的な記述方法や、個々の詳細な内容については、ページ下部の参考URLをご覧ください。テストコード自体もリファクタリングを繰り返すことで、見通しが良く保守のしやすいコードになります。

 

テストコードの構造と呼び出し順

テストコードの大枠を下に示します。実際には、省略したり、入れ子構造にすることもできるので、この通りではありません。

require 'テスト対象のファイルパス'

describe "テスト対象" do
before(:all) do
...
end

before(:each) do
...
end

it "テストの説明1" do
...
end

it "テストの説明2" do
...
end

after(:each) do
...
end

after(:all) do
...
end
end

 

このスペックを実行すると、まず最初にbefore(:all)ブロックが呼び出され、前処理を行います。次いで、各テストであるitのブロックを実行しますが、その前処理として毎回before(:each)ブロックを、後処理として毎回after(:each)ブロックを実行します。全てのテスト(example)を実行した後で最後にafter(:all)ブロックを実行し全体の後処理を行います。なお、before(:each)after(:each):eachは省略可能で、単にbefore,afterと記述することもできます。

ではテストコードの各メソッドについて説明します。

 

 

describe(とcontext)

describeメソッドでは、複数のテスト(example)を纏めるのに用います。describeメソッドの引数には、テスト対象を記述し、具体的には、テスト全体を説明する文字列やテスト対象のクラスを指定します。

describe Stack  #クラス名
...
end

describe "An empty stack" #文字列
...
end

describe Stack, "when empty" #クラスと文字列の両方指定することも可能
...
end

 

またdescribeはネストすることも可能です。例えば、メソッドごとにdescribeで整理することができます。

describe Stack do
describe "#size" do
...
end

describe "#empty" do
...
end
end

 

またdescribeのエイリアスとして、contextというメソッドがあります。describeはテストの対象を表すのに対して、contextはテストするときの状況をあらわすのに使います。

describe Stack do
context "when stack is empty" do
end

context "when stack is full" do
end
end

 

 

beforeとafter

上記「テストコードの構造と呼び出し順」で示した通り、beforeではテストを実行する前の処理、afterではテストを実行した後の処理を記述します。

 

 

it

実際のテストをitメソッドで記述します。テストの説明する文字列をitの引数として取り、テストの内容を記述したブロックを渡します。テストの内容は、プログラムに期待する動作結果を記述するため、エクスペクテーションと呼ばれます。
itメソッドの文字列引数を省略すると、エクスペクテーションの内容に従って、記述を仕様書のように組み立てて出力します。またブロックを省略すると、pending(実装を保留)されたテストとして指定でき、実行結果はテストの成功・失敗とは別に扱われます(pendingというメソッドを使って明示的に評価を保留させることもできます)。

 

<stack_spec2.rb>

# coding: utf-8
require './stack'

describe Stack do
context "新しく生成したとき" do
before do
@stack = Stack.new
end

it "スタックが空であること" do
@stack.should be_empty
end

it { @stack.should be_empty }

it "サイズが0であること"
end
end

 

ペンディングされたテストについては、黄色で表示されます。

<実行結果>

C:\ruby\rspec_test>rspec -fd stack_spec2.rb

Stack
新しく生成したとき
スタックが空であること
should be empty
サイズが0であること (PENDING: Not yet implemented)

Pending:
Stack 新しく生成したとき サイズが0であること
# Not yet implemented
# ./stack_spec2.rb:17

Finished in 0.03084 seconds
3 examples, 0 failures, 1 pending

C:\ruby\rspec_test>

 

またitの引数にタグを付けることによって、実行したいテスト対象(example)をフィルタリングすることもできます。rspecコマンドの実行時は、--tagで指定します。

<filtering_spec.rb>

describe "Filtering" do
it "test for ruby 1.8", :ruby => "1.8" do
end

it "test for ruby 1.9", :ruby => "1.9" do
end
end

 

<実行結果> 

C:\ruby\rspec_test>rspec -fd --tag ruby:1.8 filtering_spec.rb
Run options: include {:ruby=>1.8}

Filtering
test for ruby 1.8

Finished in 0.01001 seconds
1 example, 0 failures

C:\ruby\rspec_test>

 

下の例は、タグ付けの別の方法です。 

<filtering_spec2.rb>

describe "Filtering" do
it "does something" do
end

it "does something more", :focus => true do
end
end

 

<実行結果>

C:\ruby\rspec_test>rspec -fd --tag focus filtering_spec2.rb
Run options: include {:focus=>true}

Filtering
does something more

Finished in 0.01001 seconds
1 example, 0 failures

 

 

エクスペクテーション(itの中)

itのブロック内には、プログラムに期待する動作結果を記述します。具体的には、shouldまたはshould_notメソッドと共に、下のように使用します。shouldは条件を満たすことを期待するときに用い、should_notは満たさないことを期待するときに用います。否定演算子(「!=」等)は使用できませんので、should_notを使ってください。

(2 + 2).should > 3
"foo".should_not == "bar"

 

上記shouldの後の値を評価する演算子「>」は、一般的にRSpecではmatcherと呼ばれます。「>」や「==」などの演算子もmatcherの一つですが、他にもよく使うmatcherとして次のようなものがあります。詳しくは公式ドキュメントのBuilt in matchersを参照してください。

 matcher

 説明

a.should equal(b) 

 aとbがオブジェクトとして一致していることを期待。be(b)も同じ。
 a.should == b

aとbの値が一致していることを期待。eq(b)も同じ。
他の演算子matcherとして、<, <=, ===, =~, >, >=があります。

 a.should be_within(b).of(c)  aがb±cの中に入っている値であることを期待。
 obj.should be_true
 obj.should be_false
 obj.should be_nil
 objがtrue/false/nilであることを期待。
 obj.should be_XXX  obj.XXX?(XXX?は真偽値を返すメソッド)がtrueであることを期待。
 ary.should have(n).items
 ary.should have_at_least(n).items
 ary.should have_at_most(n).items
 aryがn個/n個以上/n個以下の要素を持っていることを期待(aryは配列などのコレクション)。itemsの代わりにnumbersでも可。
 a.should include(b)  aが配列などのコレクションオブジェクトなら、aの要素の中にbが含まれることを期待。aが文字列のときはbが部分文字列であることを期待。
 a.should match(regexp)  aが正規表現regexpにマッチすることを期待。a.should =~ regexpも同様。
 obj.should have_key(:a)  objにキー:aがあることを期待。
 a.should be_an_instance_of(Class)  aのクラスがClassクラスであることを期待。
 a.should be_a_kind_of(Class)  aのクラスがClassクラスまたはそのサブクラスであることを期待。be_anと同じ。
 a.should respond_to(*methods)  aが*methodsのメソッドを全て持つことを期待。
 a.should satisfy{|b| ...}

aがsatisfyの引数として与えられた条件を満たすことを期待。
(1+2).should satisfy{|n| n>1}など。

 expect{ 処理X }.to change(receiver, msg)

処理Xが評価された結果、receiver.send(msg)の結果が変わることを期待。
expect { array << 1 }.to change(array, :size) など。

 expect{ 処理X }.to change(receiver, msg).by(value)  処理Xが評価された結果、receiver.send(msg)の結果の差分がvalueであることを期待。
 expect{ 処理X }.to change(receiver, msg).from(before).to(after)  処理Xが評価された結果、receiver.send(msg)の結果がbeforeからafterに変わることを期待。
 expect{ 処理X }.to change{ block }  処理Xが評価された結果、blockの結果が変わることを期待。
expect { array << 1 }.to change{ array.size } など。上と同様に、byやfrom,toなどのメソッドチェインも可能。
 expect{ 処理X }.to raise_error(expected)  処理Xの評価の結果、expectedの例外クラスが発生することを期待。expectedを省略し例外が発生することだけを確認することもできる。
 expect{ 処理X }.to throw_symbol(:expected)  処理Xの評価の結果、シンボル:expectedがthrowされることを期待。expectedを指定しない場合は、何らかのSymbolがthrowされることを期待。

 

この他にもmatcherをユーザ定義して使用することも可能です。RSpec::Matchers.defineで宣言します。次の例を参考にして下さい。

RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
end

describe "Test for a multiple number" do
it { 10.should be_a_multiple_of 5 }
end

 

 

テストコードのリファクタリングに向けて

shouldのレシーバの省略

エクスペクテーションのshouldのレシーバは省略することができます。省略すると、何に対してのテストかが明確になり、テストコードの可読性が良くなります。

何も指定しない場合は、describeの引数がshouldのレシーバとなります。

describe Stack do
it { should be_empty }
end

describe 123 do
it { should be_odd }
end

 

またsubjectを使用して、shouldのレシーバを明示的にすることもできます。

describe "Number of Array elements" do
subject { [1,2,3] }
it { should have(3).items }
end

 

更に、itsメソッドを使うことで、subjectの結果に対してメソッドを呼び出し、その戻り値をshouldのレシーバとしてテストすることもできます。itsメソッドには、メソッドのSymbolを渡します。

describe "Number of Array elements" do
subject { [1,2,3] }
its(:size) { should == 3 }
end

 

 

テストコードの共有

重複したテストコードは、shared_examplesメソッドでまとめて共有することができます。まとめられたテストコードはit_behaves_likeメソッド(またはit_should_behave_likeメソッド)で呼び出します。

shared_examples "an array containing one element" do
it { should have(1).items }
end

describe ["foo"] do
it_behaves_like "an array containing one element"
end

describe [123] do
it_should_behave_like "an array containing one element"
end

 

またletメソッドを使うと、ブロックの評価値を、引数として渡されたシンボルと同名の変数に格納することができます。
letメソッドがbeforeと異なる点は、letメソッドでは遅延評価、すなわちexampleで実際に呼び出されるまで実行しないところです。letで生成されたオブジェクトは、同じテスト(example)の中では同じオブジェクトを使い続け、異なるexample内で呼ばれると変数を再設定します。

describe "User Test" do
let(:user) { User.new('john', 36) }

it { user.name.should == 'john' }
it { user.age.should == 36 }
end

 

 

スタブとモック

RSpecにはスタブとモックの機能があります。これによって、テスト対象の本物のクラスが全て定義されていなくても、そのクラスの振る舞いを真似るコードで代用しておくことで、テストを進めることができます。
スタブやモックは、テスト対象のクラスやオブジェクトを分離してテストできるという点で有効です。例えば、Railsの開発においては、コントローラのテストで実際のモデルの代わりに用いることで、コントローラがモデルを意図した通りに使っているかを検証することができます。
スタブとモックについて詳しくは、RSpec Mocksのドキュメントを参考にしてください。使い方などが詳しく説明されています。ここではスタブとモックの概要を紹介します。

 

スタブ

スタブとはテスト時の呼び出しに対して、あらかじめ用意された結果を返すものです。スタブの使用例を以下に示します。

<stub_spec.rb>

describe "Greeting Test" do
it "returns hello" do
#スタブの作成
greeting = double("greeting")

#スタブにsayメソッドを定義し、helloを返すように設定
greeting.stub(:say) { "hello" }

#戻り値のテスト
greeting.say.should == "hello"
end
end

 

<実行結果>

C:\ruby\rspec_test>rspec -fd stub_spec.rb

Greeting Test
returns hello

Finished in 0.03004 seconds
1 example, 0 failures

C:\ruby\rspec_test>

 

シンプルなスタブを生成するにはdoubleが便利です。メソッドと戻り値を同時に指定することもできます。またas_null_objectを付けると、定義されていないメソッドが指定されたときにオブジェクト自身を返すようにします(下の例ではbaz.to_iが指定されればbazを返す)。

foo = double('foo')
bar = double('bar', :size => 3, :to_s => "Bar")
baz = double('baz', :size => 3).as_null_object

 

stubメソッドで、doubleやオブジェクトやクラスにメソッドを追加できます。メソッドの引数や戻り値も指定できます。

obj.stub(:say)   #戻り値はnil
obj.stub(:say) { :hello }
obj.stub(:say).and_return "hello"
obj.stub(:say => :hello)
User.stub(:find).with(user_id).and_return user #引数あり

 

stub_chainメソッドで、メソッドチェーンをスタブ化することができます。 

receiver = Object.new
receiver.stub_chain("one.two.three").and_return(:value)
# receiver.stub_chain(:one, :two, :three).and_return(:value) でも可
receiver.one.two.three.should eq(:value)

 

 

モック

モックとは、オブジェクトに対するメソッドの呼び出し方を定義したものです。以下に例を示します。

<mock_spec.rb>

describe "Greeting Test" do
it "returns hello" do
greeting = double("greeting")

#sayメソッドが呼ばれることを期待し、その結果としてhelloを返す
greeting.should_receive(:say){ "hello" }

#sayメソッドを実行し、戻り値を検証
greeting.say.should == "hello"
end
end

 

<実行結果> 

C:\ruby\rspec_test>rspec -fd mock_spec.rb

Greeting Test
returns hello

Finished in 0.02003 seconds
1 example, 0 failures

C:\ruby\rspec_test>

 

sayメソッドが呼ばれる行をコメントアウトすると次のようにエラーになります。ここではsayが1回呼ばれることを期待していたが、0回であった(1度も呼ばれなかった)ため、エラーになったことを示しています。

<実行結果>

C:\ruby\rspec_test>rspec -fd mock_spec.rb

Greeting Test
returns hello (FAILED - 1)

Failures:

1) Greeting Test returns hello
Failure/Error: greeting.should_receive(:say){ "hello" }
(Double "greeting").say(any args)
expected: 1 time
received: 0 times
# ./mock_spec.rb:6:in `block (2 levels) in '

Finished in 0.02003 seconds
1 example, 1 failure

Failed examples:

rspec ./mock_spec.rb:2 # Greeting Test returns hello

C:\ruby\rspec_test>

 

このようにメソッドをモックとして設定するには、should_receiveメソッドを用います。次のように行います。

foo.should_receive(:method)  #methodが呼ばれることを期待
foo.should_receive(:method).with(引数) #[引数]で呼ばれることを期待
foo.should_receive(:method).with(anything()) #何らかの引数付を期待
foo.should_receive(:method).with(/abc/) #引数を正規表現で表すことも可能
foo.should_receive(:method).and_return("baz") #メソッドの結果bazを返す

 

メソッドが呼ばれる回数も設定できます。

foo.should_receive(:method).exactly(n).times #ちょうどn回を期待
foo.should_receive(:method).at_most(n).times #n回以下を期待
foo.should_receive(:method).at_least(n).times #n回以上を期待
foo.should_receive(:method).once #1回呼ばれることを期待

 

メソッドが正しい順序で呼ばれることを期待するよう設定することもできます。

foo.should_receive(:first_method).ordered
foo.should_receive(:second_method).ordered

 

 

参考URL

RSpecについて、基本から学びたいときや、更に進んだ内容を知りたいときは、以下のサイトを参考にしてください。

 

スはスペックのス
RSpecの基本的なことがとても分かりやすく解説されています。所々内容が古いので、こちらを参考にして読み替えてください。

 

RSpec の入門とその一歩先へ
3回のチュートリアルによって、スペックファイルをリファクタリングしていきます。チュートリアルの過程で、自然にRSpecが身につくようになっています。

 

RSpec Kerry Buckley IPRUG, 4 January 2011
スライド形式でRSpecの仕様を解説しており、本サイトで取り上げていない内容も多数掲載されています。使い方を知るためのリファレンスとして活用できます。

 

改めて学ぶ RSpec
入門的なRSpecを理解した上で、より良いテストコードを記述するために参考になるサイトです。

 

Relish
RSpecのドキュメントの公式サイトです。正確な情報はここから得ることができます。