Path: | TUTORIAL.ja |
Last Update: | Sat Oct 08 16:17:45 JST 2005 |
RSpecはプログラミング言語Ruby用の ビヘイビア・スペシフィケーション・フレームワークだ。 Rubyについては、www.ruby-lang.org/ を、 ビヘイビア(振舞)駆動開発(Behaviour Driven Development)については、 www.daveastels.com/index.php?p=5 の_A New Look at Test Driven Development_ を 参照してほしい。
このドキュメントの目的は、ビヘイビア・スペシフィケーション・フレームワークである RSpecを利用したビヘイビア駆動開発に興味がある人を支援することにある。 コメントや誤記についてはメールで報せてほしい(srbaker at pobox dot com)。
[訳注: 翻訳についての指摘はshintaro at kakutani dot com 宛に送ってください]
最初に行うべきことは、RSpecのインストールだ。ありがたいことに、 RSpecのGemパッケージをAslak Hellesoyが用意してくれているので、 次のコマンドを入力すればRubyGems経由でRSpecをインストールできる:
$ gem install rspec
まだRubyGemsをインストールしていないのであれば、docs.rubygems.org/ に RubyGemsのインストール方法を含むドキュメントがあるので参照してほしい。
RSpecをインストールしたら、正しくインストールされていることを確認しよう。 コマンドラインから引数無しでspecコマンドを実行すると、 次のように出力されるはずだ:
Finished in 0.002726 seconds 0 specifications, 0 expectations, 0 failures
これが見えれば、RSpecは正しくインストールされている。RSpecを使う準備が整った!
スペック(Specification)を構成する、もっともシンプルな要素は エクスペクテーションだ。エクスペクテーションは、コードの実行結果がどのように なって欲しいかを、RSpecへ伝えるためのものだ。 スペックのセットとはつまり、コードがどのように振る舞うべきかを 表すエクスペクテーションのセットである。
RSpecでは、エクスペクテーションとはメソッドである。 エクスペクテーション・メソッドはシステム内のあらゆるオブジェクトで利用できる。 たとえば、1の値が1であること、というエクスペクテーションをセットするには:
1.should_equal 1
と書く。1 + 1が2であること、というエクスペクテーションをセットするには、 次のように書く:
(1 + 1).should_equal 2
エクスペクテーション・メソッドは数がたくさんあるので、RSpecのAPIドキュメントに 目を通してほしい。ほとんどの(全てではないが)エクスペクテーション・メソッドには 否定形が用意されている。なので、2から1を引いたら2ではないこと、はこんな風に書ける:
(2 - 1).should_not_equal 2
ここまで挙げたサンプルは、すさまじくシンプルなものだが、 エクスペクテーションが簡単に書ける、ということはきちんと伝えられている。 自分のソフトウェアをスペック化する(specifying)場合であっても、 簡潔なエクスペクテーションを書くことで、ソフトウェアがどのように振る舞うべきかを スペック化(specify)できるだろう。 RSpecの残りはすべて、エクスペクテーションの編成を助けるためのものであり、 エクスペクテーションが満たされない場合に正確でわかりやすいレポートを提供する。
そろそろ、数値の足し算や引き算のエクスペクテーションばかり書いて 退屈してきたことかと思う。そうなったら、自分のコードのエクスペクテーションの 書き始めどきなのだが、もうちょっと複雑なサンプルにつきあってもらいたい。
では、ロボットを書いてみよう。 ロボットは、与えられたXY座標からスタートし、 与えられた方向に与えられた値の単位だけ移動する。 開始座標が与えられない場合は、座標0,0からスタートする。 ロボットは、移動したら2つの要素を持つ配列を返す。 配列にはX座標とY座標が格納されている。 このロボット用のエクスペクテーションには次のようなものが含まれるだろう:
rob = Robot.new rob.should_not_be_nil # [訳注]これはたぶんバグ。should_be_kind_of というexpectation methodは無い rob.should_be_kind_of Robot rob.x_coordinate.should_equal 0 rob.y_coordinate.should_equal 0 rob.location.should_equal [0,0] rob.move_north(1).should_equal [0, 1] rob.move_south(5).should_equal [0, -4] rob.move_east(10).should_equal [10, -4] rob.move_west(5).should_equal [5, -4]
このコードは、良い設計を示すためのものではなく、 エクスペクテーション・メソッドの使い方を示すものだということに注意してほしい。
また、エクスペクテーションが繰り返し記述されているが、これについても、 このドキュメントがRSpecフレームワークの紹介を目的としているからであって、 これが即ちビヘイビア駆動開発ではないことにも注意。
ご覧の通り、エクスペクテーション記述は、こんな単純なプロジェクトであっても くどくなりがちである。これをわかりやすくするためには、エクスペクテーションを スペックとしてグループ化するとよい。 次セクションではその方法を説明する。
ソフトウェアはエクスペクテーションを書くことでスペック化される(specified)。 エクスペクテーションについては前セクションで説明した。わかりやすくするために、 エクスペクテーションをスペックとしてグループ化できる。 スペックとは単なるメソッドであり、 そこに複数のエクスペクテーションを記述する。 スペックの名前は、Specランナー(「実行とレポート」のセクションを参照)が、 エクスペクテーションを満たさない場合の通知に利用する。 なので、スペック名はちゃんと考えるべきだ。
先ほどのロボットのサンプルは、いくつかのスペック・メソッドに分割できる:
def initialization_without_coordinates rob = Robot.new rob.x_coordinate.should_equal 0 rob.y_coordinate.should_equal 0 rob.location.should_equal [0,0] end def initialization_with_coordinates rob = Robot.new(10, 15) rob.x_coordinate.should_equal 10 rob.y_coordinate.should_equal 15 rob.location.should_equal [10, 15] end def north_movement rob = Robot.new rob.move_north(1).should_equal [0, 1] end def west_movement rob = Robot.new(10, 5) rob.move_west(15).should_equal [-5, 5] end
エクスペクテーションを分割してスペック・メソッドに記述することで、 ソフトウェアの振る舞いが読みやすくなり、意図も明確に表現できる。 Rubyに特別詳しくない人(得てしてそういう人はソフトウェア開発全般にも相対的に 明るくなかったりする)であっても、我々のスペックであれば、 簡単に読めるだろう。
先ほどから例に出しているサンプルでは、ロボットの生成コードが重複していることに お気づきかと思う。スペックを書く際に、オブジェクトの初期化コードは 重複しがちである。RSpecではこうしたセットアップに対応するために2つのメソッドを 用意している。setupメソッドとteardownメソッドだ。
setupメソッドは、各スペックの実行前に呼び出されるので、 各スペックが必要とするリソースをセットアップできる。 一方、teardownメソッドは各スペックの実行後に呼び出されるので、 スペックで必要だったリソースのデアロケートやクローズを適切に行える。
次セクションのサンプルではフィクスチャとコンテキストの使い方を示す。
スペックのサンプルでは、コンテキストの出番はない。 スペックの記述が膨大になり、スペックを論理的なグループへと 分割したくなったときこそが、コンテキストの出番だ。 スペック・メソッドをContextのサブクラスに記述することでグループ化できる。 コンテキストは好きなだけ定義できる。適切な粒度で定義された(well-defined)、数多くの コンテキストへと分割することをお勧めする。
以下のコンテキストはロボットの移動についてのスペック一式だ。 実行すると、いい感じに出力される。
require 'spec' class RobotMovement < Spec::Context def setup @rob = Robot.new # [訳注]: ここもたぶんバグ。正しくは「Robot.new(10, 5)」 @rob1 = Robot.new(10, 15) end def movement @rob.move_north(1).should_equal [0, 1] @rob1.move_west(15).should_equal [-5, 5] end def teardown @rob.die @rob1.die end end
RSpecは、スペックに名前スキーマを必要としない。 コンテキストが実行されると、コンテキストに定義されているすべてのpublicメソッドが 呼び出される。 ただし、RSpecが内部的に呼び出しているメソッドは例外である。 スペック名としての利用が禁止されているメソッドは、 initialize、mock、violated、runだ。 Rubyの動的な性質により、これらの利用禁止メソッドをオーバーライドしたとしても 何も警告はされない。ただ実行結果が想定していないものとなるだけである。
前述した4つの利用禁止メソッド名以外にも制限はある。 アンダースコアで始まるメソッドは、スペックとしては実行されない。 これを利用して、コンテキスト用にメソッドを定義したり、 スペックを実行させないようにしたりできる。
RSpecにはいまのところ、’spec’と名づけられたコマンドラインのRSpecランナーが用意 されている。スペックを書いたファイルをコマンドライン経由で RSpecランナーに渡すと、スペックが実行され、 エラーの発生した箇所が通知される。コマンドラインのRSpecランナーを使うにあたっては、 まずはRSpecが提供しているサンプルを実行させてから、 自分の書いたスペックを実行することをお勧めする。
出力のサンプルとして、RSpecのexamplesに収録されているmovie_spec.rbファイルを実行した 場合を示す。以下のような出力が得られるはずだ:
$ spec movie_spec.rb .... Finished in 0.016 seconds. 4 specifications, 4 expectations, 0 failures.
specはスペックの数をカウントし、それを単一のドット(.)として画面に表示する。 失敗(failure)した場合は「X」が表示される。失敗するごとに、specはどこで失敗したかを 示すバックトレースを表示する。失敗の基本は例外の発生だ。 例外の発生源は、スペック対象のソフトウェアである場合もあれば、 Ruby自身である場合や(SyntaxError、RuntimeError)、 エクスペクテーションを満たしていない場合がある(ExpectationNotMet)。
たとえば、OneMovieListには"Spece Balls"が含まれていることを期待しているのだが、 実際に含まれていたのは"Star Wars"だったとしよう。 その場合には次のようなエラーが出力される:
$ spec movie_spec.rb X... 1) <#<MovieList:0x284c1e0 @movies={"Star Wars"=>#<Movie:0x284c180 @name="Star Wars" >}>> should include <"Space Balls"> (Spec::Exceptions::ExpectationNotMetError) ./movie_spec.rb:34:in `should_include_space_balls' ../bin/spec:10 Finished in 0.016 seconds 4 specifications, 4 expectations, 1 failures
この通知は、MovieListオブジェクトは"Space Balls"を含むべきだったのだが、 ExpectationNotMetエラーが発生したことを表わしている。 続いて表示されているバックトレースは、 ‘moview_spec.rb’の34行目の’should_include_space_balls’メソッドが エクスペクテーションを満たしていないことを示している。
このドキュメントは全体として、ビヘイビア駆動開発の概要を 説明するものではなく、RSpecに特化している。 RSpecを利用したソフトウェアの振る舞いをスペック化(specify)できるようになることを 目的としたチュートリアルである。 より詳しい情報はRSpecのAPIドキュメントに記述されている。 BDDやRSpecについての追加のドキュメントを現在準備中である。
このチュートリアルの翻訳はかくたに(kakutani.com/)が行いました。
訳語についてbabieさん、オブジェクト倶楽部の天野勝さん、懸田剛さんに助言をいただきました。 ありがとうございます。