プログラムファイルにスペックを組み込む

Rubyベストプラクティス -プロフェッショナルによるコードとテクニックRubyベストプラクティス」(p.30, 1.5.1 ライブラリファイルにテストを組み込む)に、プログラムが小さいときにプログラム本体とテストのコードを同居させる話が載ってた。いつもの「if $0 == __FILE__」節にテストコードを(「require 'test/unit'」と共に)書いておくというものだった。もともと Test::Unit (MiniTest::Unit) は rubyスクリプトとして実行するものだったからそれで良いけど、RSpec ではどうしたら良いんだろう。それにコマンドラインからの実行がテストだけになるのもちょっとね。
大体スペックのコードは Ruby互換ではあるが specコマンドでの実行を前提にしていて rubyで実行しようとしてもそんなメソッドないとかエラーになる。何か require しようにももともとそういうものじゃない。どうしよう。
というわけでもともとのプログラムファイルの構成をこんな感じにする。

<プログラム本体>

if $0 == __FILE__ then
  <コマンドライン実行時の動作>
end # if $0 == __FILE__

__END__

<スペック>

こうすると ruby コマンドでの実行時は __END__行以降は無視してくれるので大丈夫、__END__行以降にプログラム用のデータを置いとくとか出来なくなるけどそれはまあ。
で、スペック実行時は __END__ を削除して実行すれば

ruby -rtempfile -e 'Tempfile.open(File.basename((r = ARGV.shift), "rb")){|t| File.read(r).sub("__END__","").display t;t.close false;`spec #{t.path} #{ARGV.join(" ")}`.display}' <ファイル名> <specオプション>

削除したものを一度テンポラリファイルに書き出し、所要のオプションを付けて specコマンドで実行するワンライナー。こうすると「if $0 == __FILE__」節には普通にコマンドライン実行時の動作を書ける。
どんなもんでしょう。

ローマ数字

下記を書く時に考えた。ちょっと時間が経ってるけど「日本Rubyの会 公式Wiki - 第42回 Ruby/Rails勉強会@関西」(第42回 Ruby/Rails勉強会@関西(運営:Ruby 関西)出席 - Rubyとか Illustratorとか SFとか折紙とか)の okkez さんによる初級者レッスンの課題、整数をローマ数字で表現する。メソッド一つ定義するだけにスペックファイル(とレイクファイル)別に作るのもなんだなあと上記ベストプラクティスに思い当たった。

#!/usr/bin/ruby
# conding: utf-8

class Integer
  Roman = [
    %w[1000 M],
    %w[500 D],
    %w[100 C],
    %w[50 L],
    %w[10 X],
    %w[5 V],
    %w[1 I],
    ]
  def to_roman
    roman, = Roman.inject(['', self]) do |result, num_letter|
      [
        result[0] + num_letter[1] * (result[1] / num_letter[0].to_i),
        result[1] % num_letter[0].to_i,
      ]
    end #roman, = Roman.inject(['', self]) do |result, num_letter|
    Roman.each_cons(3) do |major, half, minor|
      next unless half[0][0, 1] == '5'
      roman.sub! half[1] + minor[1] * 4, minor[1] + major[1]
      roman.sub! minor[1] * 4, minor[1] + half[1]
    end #Roman.each_cons(3) do |major, half, minor|
    roman
  end #def to_roman
end #class Integer

if $0 == __FILE__ then
  ARGV.each{|arg| arg.to_i.to_roman.+("\n").display }
end #if $0 == __FILE__

__END__

describe Integer do
  it 'should be true' do; Integer.should be_true; end

  context '#to_roman' do
    subject{ rand 100 }
    it{ should respond_to(:to_roman) }
    it{ lambda{ subject.to_roman }.should_not raise_error }

    describe Bignum do
      subject{ 2**62 }
      it{ should be_a_instance_of(Bignum) }
      it{ should respond_to(:to_roman) }
      #it{ lambda{ subject.to_roman }.should_not raise_error }
      it{ lambda{ subject.to_roman }.should raise_error(NoMemoryError) }
    end #describe Bignum do
  end #context '#to_roman' do
end #describe Integer do

describe 1 do subject{ 1 }
  its('to_roman'){ should == 'I' }
end #describe 1 do subject{ 1 }

[
  %w[1 I],
  %w[2 II],
  %w[3 III],
  %w[4 IV],
  %w[5 V],
  %w[6 VI],
  %w[7 VII],
  %w[8 VIII],
  %w[9 IX],
  %w[10 X],
  %w[11 XI],
  %w[12 XII],
  %w[13 XIII],
  %w[14 XIV],
  %w[15 XV],
  %w[16 XVI],
  %w[19 XIX],
  %w[20 XX],
  %w[40 XL],
  %w[50 L],
  %w[60 LX],
  %w[90 XC],
  %w[99 XCIX],
  %w[100 C],
  %w[200 CC],
  %w[400 CD],
  %w[500 D],
  %w[600 DC],
  %w[900 CM],
  %w[1000 M],
  %w[1999 MCMXCIX],
  %w[2000 MM],
  %w[3000 MMM],
].each do |num, roman|
  describe num do subject{ num.to_i }
    its('to_roman'){ should == roman }
  end #describe num do subject{ num.to_i }
end # each do |num, roman|