こんばんは。
学習は、15日目です。前回の続き、自動テストについてです。
テスト駆動開発(TDD)と振舞駆動開発(BDD) - ダメ人間が生きている
今日は、RSpecによるテストを書いていきます。
testinitで書いたテストをRSpecで書いてみる
前回testinitで作成した、引数が"shigotop"か否かを判定するメソッドis_shigotopに対するテストを、今回はRSpecで書いてみます。
require "./spec_helper.rb" require "./nameinspecter.rb" describe NameInspecter do before do obj = NameInspecter.new end it "shigotoはfalse" do @obj.is_shigotop('shigoto').should be_false end it "shigotopはtrue" do @obj.is_shigotop('shigotop').should be_true end it "shigotopoはfalse" do @obj.is_shigotop('shigotopo').should be_false end end
文法とかも、前回ブログ参照。
[chanco:spec]rspec name_spec.rb Run options: include {:focus=>true} All examples were filtered out; ignoring {:focus=>true} FFF Failures: 1) NameInspecter shigotopoはfalse Failure/Error: @obj.is_shigotop('shigotopo').should be_false NoMethodError: undefined method `is_shigotop' for nil:NilClass # ./name_spec.rb:18:in `block (2 levels) in <top (required)>' 2) NameInspecter shigotopはtrue Failure/Error: @obj.is_shigotop('shigotop').should be_true NoMethodError: undefined method `is_shigotop' for nil:NilClass # ./name_spec.rb:14:in `block (2 levels) in <top (required)>' 3) NameInspecter shigotoはfalse Failure/Error: @obj.is_shigotop('shigoto').should be_false NoMethodError: undefined method `is_shigotop' for nil:NilClass # ./name_spec.rb:10:in `block (2 levels) in <top (required)>' Finished in 0.00091 seconds 3 examples, 3 failures Failed examples: rspec ./name_spec.rb:17 # NameInspecter shigotopoはfalse rspec ./name_spec.rb:13 # NameInspecter shigotopはtrue rspec ./name_spec.rb:9 # NameInspecter shigotoはfalse Randomized with seed 43035
エラーです。
「nilに対するis_shigotopというメソッドは定義されてません」とのこと。
テストコード中でis_shigotopに引数として与えている文字列が、nilと認識されてるのでしょうか。
とおもいきや、before内で、objとしているところに@が抜けていました。
@も含めたクラス内変数、というかんじですね。
[chanco:spec]rspec name_spec.rb Run options: include {:focus=>true} All examples were filtered out; ignoring {:focus=>true} ... Finished in 0.00483 seconds 3 examples, 0 failures Randomized with seed 65395
無事、通りました。
本来は、通らないべきですが、ここは割愛。
Rubyの課題のテストをRSpecで書いてみる
次は、Rubyのときにやったプログラムの、テストを書きます。
Rubyのインストールと基本 - ダメ人間が生きている
と、ここで問題が。
上記課題で書いたプログラム、OrangeTreeクラスとDragonクラスの課題以外、すべて処理の羅列です。
オブジェクト指向皆無のプログラムです。
なので、RSpecでテストできません。
できるのかもしれませんが、やりません。
というわけで、OrangeTreeクラスをテストしてみる。
まず、「一年経過」。
oneYearPassesメソッド実行後、インスタンス変数@yearがインクリメントされてるか。
it "一年経過" do @tree.oneYearPasses @tree.instance_variable_get(:@year).should == 1 end
インスタンス変数は、instance_varible_getメソッドで取得できます。
特に問題ありません。
次に、「七年経過で木が死ぬ」。
七年経過したら、標準出力に「木は死にました」と表示し、exitするかテストします。
標準出力のテスト
標準出力をテストするには、どうすればいいのでしょうか。
RSpecで標準出力をテストしたい - Qiita
ここでやっていることが、ぼくには理解できません。
コピペしたら案の定エラー出たので、文法を理解します。
上記サイトで、以下の処理が意味不明です。
capture(:stdout) { @target.hello }.should == 'hello\n'
焦らず、順番に、解読していきます。
シンボルと文字列の違い
まず、:stdoutの:とは??シンボルと呼ぶらしいですが。
Ruby のシンボル | すぐに忘れる脳みそのためのメモ
シンボルとは、Ruby が内部でメソッド名などの識別に使っている数値で、任意の文字列に対して異なった値が割り当てられます。
文字列とおなじ感覚で使えますが、文字列との違いとしては、例えばいったん:xが定義されると、どこから参照しても、:xは同一のオブジェクトを指します。たぶん。
つまり、:stdoutは、標準出力を参照するシンボルとしてRuby内部で定義されているっぽいです。
メソッドにブロックを渡す
{ @target.hello }は、ブロックです。たぶん。
わからないのは、captureメソッド(引数) {ブロック}、というつながりです。
ここでブロックは、なんなんでしょうか。
Minituku - a Ruby e-learning system
ブロック付きメソッド呼出しをおこなった時に、メソッドの後ろに付けるブロックはRubyの特徴的な機能のひとつです。
yieldを使うことによって、ブロック付きメソッド呼出しの時にブロックの中のプログラムの処理をおこないます。
定義したメソッドを呼び出した時にyieldがメソッドの中にないと、ブロックの中の処理をおこないません。yieldがないとブロックの中にある処理は無視します。メソッドの中にyieldがある時は、ブロックの中へ処理を移します。ブロックの中の処理をすべておこなってから、またメソッドの中に戻り、yieldの下から処理をおこないます。
つまり、上記の処理の流れは以下になります。
- 標準出力を引数としてcaptureメソッドに渡す
- captureメソッドの処理を、yieldの直前まで実行する
- @target.helloを実行する
- captureメソッドのyield以降の処理を実行する
次に、captureメソッドの中身を見ていきます。
def capture(stream) begin stream = stream.to_s eval "$#{stream} = StringIO.new" yield result = eval("$#{stream}").string ensure eval "$#{stream} = #{stream.upcase}" end result end
例外処理
begin、ensureは例外処理です。
制御構造
begin以降の実行中に例外が発生した場合でも、begin式終了前にensure節を評価します。begin式全体の評価値は、本体/rescue節/else節のうち 最後に評価された文の値であり、ensure節の値は無視されます。
これだけではよくわからないので、さらにその中身を見ていきます。
コード内での展開
evalを使うと実行時にその場で文字列をコンパイルし、評価することができる。 返り値はそのプログラムの最後の式の値だ。
また、$は、グローバル変数、#{}はリテラルの式展開です。つまり、eval "$#{stream} = StringIO.new"は、eval "$(:stdout.to_s) = StringIO.new"というかんじになります。グローバル変数である:stdoutに対して、StringIOインスタンスを代入します。
以上より、captureメソッドのの中身も含めた処理の流れは、以下になります。
- 標準出力のハンドラをcaptureメソッドに渡す
- 標準出力のハンドラに、文字列処理のインスタンスを代入する
- @target.helloを実行し、標準出力に"hello"と出力する
- 標準出力のハンドルから"hello"を取得し、その文字列を返す
やっと解読できました。。
exitのテスト
bear.mini : RSpec で exit したかどうかをチェックする方法
ここにそって、テストを書きます。lambda{}は、Proc.new{}と同じ(厳密には違う)で、ブロックをオブジェクトにします。詳しくは、こちら。
というわけでテストを書いてみる
長くなりましたが、ここまでのテストを書いてみます。「一年たったら一つ歳をとる」「七歳で木が死んでメッセージ表示」「七歳で木が死んでexit」のテストです。
require "./spec_helper.rb" require "../lib/orange_tree.rb" require "stringio" def capture(stream) begin stream = stream.to_s eval "$#{stream} = StringIO.new" yield result = eval("$#{stream}").string ensure eval "$#{stream} = #{stream.upcase}" end result end describe OrangeTree do before :each do @tree = OrangeTree.new end it "一年たったら一つ歳をとる" do @tree.oneYearPasses @tree.instance_variable_get(:@year).should == 1 end it "七歳で木が死んでメッセージ表示" do 6.times do @tree.oneYearPasses end capture(:stdout){@tree.oneYearPasses}.should be_include '木は死にました' end it "七歳で木が死んでexit" do 6.times do @tree.oneYearPasses end lambda {@tree.oneYearPasses}.should raise_error(SystemExit) end end
実行結果は、以下のとおりです。
[chanco:spec]rspec orange_tree_spec.rb Run options: include {:focus=>true} All examples were filtered out; ignoring {:focus=>true} .F木は死にました . Failures: 1) OrangeTree 七年で死んでメッセージ表示 Failure/Error: capture(:stdout){@tree.oneYearPasses}.should == '木は死にました' SystemExit: exit # /Users/kabata/Documents/intern/rspec_exercise/lib/orange_tree.rb:16:in `exit' # /Users/kabata/Documents/intern/rspec_exercise/lib/orange_tree.rb:16:in `oneYearPasses' # ./orange_tree_spec.rb:31:in `block (3 levels) in <top (required)>' # ./orange_tree_spec.rb:9:in `capture' # ./orange_tree_spec.rb:31:in `block (2 levels) in <top (required)>' Finished in 0.00187 seconds 3 examples, 1 failure Failed examples: rspec ./orange_tree_spec.rb:27 # OrangeTree 七年で死んでメッセージ表示 Randomized with seed 42776
エラーです。exitしてしまうので、captureできない、というかんじでしょうか。
ためしに、テスト対象のorange_tree.rbでexitしないように書き換えると、とおりました(当然、代わりにexitの方のテストが引っかかります)。
exitするときって、たいがい何かしらのメッセージをputsして終わると思うんですが、どうやってテストすればいいんでしょうか。
Capturing Standard Out In Unit Tests - Pivotal Labs
こちらを参考に、標準出力を文字列にリダイレクトするよう、テストを修正しました。
--略-- it "七年で死んでメッセージ表示" do 6.times do @tree.oneYearPasses end output = capture(:stdout) do lambda{@tree.oneYearPasses}.should raise_error(SystemExit) end output.should be_include '木は死にました' end
無事、テストを通りました。
OrangeTreeクラスのSpec
というわけで、全体のSpecを作成しました。長いのでgithubで。
rspec_exercise/orange_tree_spec.rb at master · shigotop/rspec_exercise · GitHub
実行してみました。
[chanco:spec]rspec orange_tree_spec.rb Run options: include {:focus=>true} All examples were filtered out; ignoring {:focus=>true} FF..... Failures: 1) OrangeTree 木になっているオレンジの数を返す Failure/Error: @tree.countTheOranges.should == orange expected: 3 got: nil (using ==) # ./orange_tree_spec.rb:52:in `block (2 levels) in <top (required)>' 2) OrangeTree 三年目から毎年年齢と同じ数の実をつける Failure/Error: @tree.instance_variable_get(:@orangeCount).should == 0 expected: 0 got: -1 (using ==) # ./orange_tree_spec.rb:39:in `block (3 levels) in <top (required)>' # ./orange_tree_spec.rb:38:in `times' # ./orange_tree_spec.rb:38:in `block (2 levels) in <top (required)>' Finished in 0.00459 seconds 7 examples, 2 failures Failed examples: rspec ./orange_tree_spec.rb:47 # OrangeTree 木になっているオレンジの数を返す rspec ./orange_tree_spec.rb:37 # OrangeTree 三年目から毎年年齢と同じ数の実をつける Randomized with seed 28518
2つ、バグが見つかりました。
ひとつめは、returnする変数名が間違っていました。ふたつめは、実がなりだす(3年目から)前から、「オレンジの数を前年なった個数分減らす」処理をしていました。
ふたつめの修正の影響で、
rspec ./orange_tree_spec.rb:68 # OrangeTree ある年に取り残したオレンジは次の年に落ちる
となりました。さっきこれがとおっていたので、テストが間違っていたということです。最悪のケースですね。反省します。全体を修正し、無事テストをとおりました。これでデバッグが完了です。
もうひとつ、Dragonクラスもありましたが、疲れたので、とりあえずここまで。githubにpushして確認してもらいます。
(課題クリアに必要なら全課題についてテスト書きますサボりですすいません)
まとめ
合計、5時間くらいです。
わからないところを調べる時間が長くなってしまい、あまり進みませんでした。
RSpecについて
まず、テストを書くのが、難しいです。RSpecをマスターするまで、調べながら、やっていくしかなさそうです。焦らず、まずテストをしっかりとつくり、コードを書いていく、というのがよさそうです。なかなかできませんが。テストを書く練習をめんどくさいと思ってしまう時点で、危ういですね。
プログラム書くの楽しいですが、ちょっと長くなると、とたんに把握できなくなります。そういう意味でも、プログラムを個々の部位に分けて、作っていく、TDD(BDD?)を、はやくマスターしたいです。
今後について
そろそろ頭の容量が追いつかなくなってきましたが、次回は、いよいよ待ちに待ったRailsです。Ruby on Rails チュートリアル:実例を使って Rails を学ぼうを進めていきます。楽しみです。その前に、これまでの全体を復習しておこうと思います。ここまで、十分な理解が得られているのか、不安です。