読者です 読者をやめる 読者になる 読者になる

No Bugs, No Life

読んだ本や、プログラミング、システム開発等のねたを中心に。文章を書く練習なので少し硬派に書くつもりだけど、どうなることやら。

RSpec書き始め

MW DEV Ruby

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)でインスタンスを返すことを想定している。シーケンス図としてどのように表現すべきかがわからなかったのでとりあえずこのように表現している。
f:id:kazyury:20130503233454p:plain

素材とメモの抽出器(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を埋めるのは次回に。

*1:やはりRails関連が多いが...。

*2:ちなみにrspecにはコメントは書かないが、rubyjavaもコメントは積極的に日本語を書くようにしている。なんとなく日本語で記載した方がその部分がコメントとして認識しやすいような気がするので

*3:Procのほうが良かったかな?