RSpec書き始め
MementoWeaver開発記
Vimの環境とRSpecを書き始めるための環境も少しずつだが整備できてきたので、そろそろ本腰を入れてRSpecを書き始めてみる。
RSpecに関する情報収集
RSpecについては殆ど何も知らない状況なので、まずは情報収集から始める。
URL | 説明 | 備考 |
---|---|---|
RSpec.info: home | 本家 | RDocとRelishAppへの入り口程度 |
RSpec | RSpec-2全体の鳥瞰に向いたプレゼン | Kerry Buckley Jan.2011 |
RSpec Core 2.13 - RSpec Core - RSpec - Relish | RSpec-2の公式ドキュメント? | RelishApp(RSpec-Core) |
Documentation for rspec-core (2.13.1) | RDocだがREADMEがエントリポイントで判りやすい | RDoc |
RSpec Expectations 2.13 - RSpec Expectations - RSpec - Relish | RSpec-2の公式ドキュメント? | RelishApp(RSpec-Expectations) |
Documentation for rspec-expectations (2.13.0) | Matcherのリファレンスとして使いやすい | RDoc |
Rubyist Magazine - 改めて学ぶ RSpec | 書き方の取っ掛かりに | Rubyist Magazine記事 |
Rubyist Magazine - 海外記事翻訳シリーズ 【第 1 回】 RSpec ベストプラクティス | 書き方の取っ掛かりに | Rubyist Magazine記事 |
Rubyist Magazine - スはスペックのス 【第 1 回】 RSpec の概要と、RSpec on Rails (モデル編) | Rails中心かな? RSpec1系なので古い記述あり | Rubyist Magazine記事 |
RSpecによるユニットテストの書き方 - tech.recompile.net | 書き方の取っ掛かりに | Tumblr様記事 |
RSpec の入門とその一歩先へ - t-wadaの日記 | RSpecをお題にしたTDD 写経 | t-wada様記事 |
上記に加えて、QiitaのRSpecタグ(Rspecに関する新着投稿 - Qiita [キータ])辺りにも有用な情報がストックされている*1。日本語の情報量が少ない中でQiitaのストックには助けられている。
スペックの書き方の方針
describe/context/it の構造
上記の参考資料のうち、特にTumblr様の記事(RSpecによるユニットテストの書き方 - tech.recompile.net)に納得させられたので、describe/context/itは、基本的には以下の構造をとる(しばらくやってみて馴染まないようなら考え直すかも知れないが)。
ただし、'テスト対象'の部分は文字列ではなく、テスト対象のクラス名を置く。
describe 'テスト対象' do context '状態' do describe 'テスト対象メソッド' do context '与える入力' do it '期待する出力' end end end end
(※ 上記の構造はTumblr様の記事より引用しています)
expect or should ?
RSpec-ExpectationsのRDOCにも記載されているが、従前のシンタックス(should/should_not)に加えて、expectシンタックスが2.11.0から追加されている。
In addition to the should syntax, rspec-expectations supports a new expect syntax as of version 2.11.0:
http://rubydoc.info/gems/rspec-expectations/frames
また、理由は詳しくはMyron Marston » RSpec's New Expectation Syntaxに記載されているが、要するにmethod_missingとかdelegateとかを使用しているクラスではshould/should_notが正常に動作しない場合があるらしい。
We added the expect syntax to resolve some edge case issues, most notably that objects whose definitions wipe out all but a few methods were throwing should and should_not away. expect solves that by not monkey patching those methods onto Kernel (or any global object).
http://rubydoc.info/gems/rspec-expectations/frames
特に既存のRSpecコードがあるわけではないので、MWの移行プログラム(mwmig)ではおとなしくexpectシンタックスを使用することにする。
英語 or 日本語
"「日本語も使えるので日本人プログラマにとっても判りやすい」ような仕組み"には今まで良い記憶は無いのだが、英語力にも自信が無いので、スペックファイル中のContextとitには積極的に日本語を記載することにする*2。
逆にdescribeには基本的にクラス名とメソッド名ぐらいしか記述しないようにする。
ただし、メソッド名を表現するdescribeとそのメソッドに対する入力を表現するcontextの間に入れたい助詞程度はメソッド名のdescribe側に入れてもOKとする(自宅プロジェクトなので基本的にはゆるい)。
例えば、スペックを「MementoBody.get_body()を引数なしで実行した場合RuntimeError例外を送出する」としたい場合、以下の2通りの書き方が考えられる。
最初の例は助詞をメソッド引数を表現するcontext側に入れた場合、2番目の例は助詞をメソッド名のdescribe側に入れた場合。
describe MementoBody do describe '.get_body()' do context 'を引数なしで実行した場合' do it 'RuntimeError例外を送出する' end context 'を"<body>"と"</body>"を含まない文字列を引数として実行した場合' do it 'RuntimeError例外を送出する' end end end
describe MementoBody do describe '.get_body()を' do context '引数なしで実行した場合' do it 'RuntimeError例外を送出する' end context '"<body>"と"</body>"を含まない文字列を引数として実行した場合' do it 'RuntimeError例外を送出する' end end end
何れもrspec -fdした時のスペックとしては「MementoBody.get_body()を引数なしで実行した場合RuntimeError例外を送出する」と読めるが、スペックファイル上でのcontextの行が1番目の例(context 'を引数なしで実行した場合')だと、contextを実行した場合のように見えてしまって一寸気持ち悪い。
ここら辺ももう少しこなれてきたら見直すかもしれないが、ひとまずはこのルールで進めてみる。
mwmigの基本的なスペック
そろそろmwmigの具体的なスペックを書き始める。
まずは、素材とメモの抽出器から。
素材とメモの抽出器(MementoBody)関連のシーケンス図
最初の例としてはアルバムタイプのメメントから素材パスと素材のメモを抽出する抽出器の挙動をザックリと以下のような形とする。UMLとしての正しさはかなりいい加減なのだけど、rubyでブロックを渡す場合のシーケンスってどのように書くのが適切なんだろう?
この図ではとりあえずlambda*3を呼び元("AP"と表現)で作成しておいて、そのlambdaを呼ばれた側(MementoBody)がcallするような形で表現している。
あと、MementoBodyFactoryなるクラスを書いているが、実際にはMementoBody.get_body(str)でインスタンスを返すことを想定している。シーケンス図としてどのように表現すべきかがわからなかったのでとりあえずこのように表現している。
素材とメモの抽出器(MementoBody)のスペック
上記のシーケンスだと、MementoBodyのスペックとして.get_bodyと#each_photo_divの2メソッドについて記載することになる。
1st cutのスペックは以下のようにした。
- .get_bodyではMementoBodyクラス自体の状態は問わないため'状態'を表すcontextは省略した。
- #each_photo_divは引数なしのブロック付きメソッドとすることを想定しており、'与える入力'のcontextは省略した。
describe MementoBody do describe '.get_body()を' do context '引数なしで実行した場合' do it 'RuntimeError例外を送出する' end context '"<body>"と"</body>"を含まない文字列を引数として実行した場合' do it 'RuntimeError例外を送出する' end context '"<body></body>"を含む文字列を引数として実行した場合' do it 'MementoBodyインスタンスを返却する' end context '"<body>...(改行を含む任意文字)...</body>"を含む文字列を引数として実行した場合' do it '改行文字を含む場合であってもMementoBodyインスタンスを返却する' end end end describe MementoBody,'インスタンスが' do context 'class属性がphotoのdivタグを持たない場合' do describe '#each_photo_div' do it 'はnilを返却する' end end context 'class属性がphotoのdivタグを1つ含む場合' do describe '#each_photo_div' do it 'はPhotoDivインスタンスを1回yieldする' end end context 'class属性がphotoのdivタグを2つ含む場合' do describe '#each_photo_div' do it 'はPhotoDivインスタンスを2回yieldする' end end end
長くなってきたので実際にexampleを埋めるのは次回に。