Rubyスクリプトのテスト技法

Rubyスクリプトを効果的にテストする「RubyUnit」を試してみる。

テストって重要?

Rubyは書きやすいし,高機能なので短いスクリプトで狙ったものが書ける。だから楽しい。だからかもしれないけど,動いてるスクリプトでもリファクタリング(Refactoring)することが多いように思う。

で,うっかり振る舞いを変えてしまったりすることがある。Rubyは何でもかんでも実行時に解決するので,実行してみないと発見が難しい。

で,テストフレームワーク(Testing Framework)の出番。Rubyスクリプトの各パーツの挙動を調べるテスト・スクリプトを用意し,そのテストに合格しないといけない,ということにすれば,安心して内部を触ることができる。

テストの書き方

簡単なスクリプトを書いてみる。

sample-1.1.rb
  5: class Account
  6:   def initialize
  7:     @balance = 0
  8:   end
  9:   def deposit(amount)
 10:     @balance += amount
 11:   end
 12:   def withdraw(amount)
 13:     @balance -= amount
 14:   end
 15:   attr_reader :balance
 16: end
 17: 
 18: if __FILE__ == $0
 19:   a = Account.new
 20:   a.deposit 100
 21:   a.withdraw 50
 22:   puts a.balance
 23: end

Accountは口座を模したクラス。depositメソッドで預けてwithdrawメソッドで引き出す。balanceメソッドでその時点の残高を得る。

実行結果
50

このスクリプトをテストするテストスクリプトを書いてみる。

sample-1.1-test.rb
  4: require "runit/testcase"
  5: require "runit/cui/testrunner"
  6: 
  7: require "sample-1.1.rb"
  8: 
  9: class AccountTest < RUNIT::TestCase
 10:   def test1
 11:     a = Account.new
 12:     a.deposit 100
 13:     a.withdraw 30
 14:     assert_equal(70, a.balance)
 15:   end
 16: 
 17:   def test2
 18:     a = Account.new
 19:     a.deposit 50
 20:     a.withdraw 80
 21:     assert_equal(-30, a.balance)
 22:   end
 23: end
 24: 
 25: RUNIT::CUI::TestRunner.run(AccountTest.suite)

テストは,RUNIT::TestCaseクラスから派生したテストクラスに記述する。メソッド名がtestで始まるメソッドが名前の順番に呼ばれる。

テストクラスでは,assert_equalメソッドなどでテストを行う。test1, test2でそれぞれ,残高が期待する値になるかをテストしている。

テストの実行は,RUNIT::CUI::TestRunnerを用いる。

実行結果;
AccountTest#test1 .
AccountTest#test2 .
Time: 0.014404
OK (2/2 tests  2 asserts)

正しくテストにパスした。

動作を変更するとき

このAccountクラスの動作を変更し,マイナスの預け入れや残高がマイナスとなるような引き出しができないようにしてみる。

先にテストクラスを修正する。

sample-1.2-test.rb
  4: require "runit/testcase"
  5: require "runit/cui/testrunner"
  6: 
  7: require "sample-1.1.rb"
  8: 
  9: class AccountTest < RUNIT::TestCase
 10:   def test1
 11:     a = Account.new
 12:     a.deposit 100
 13:     a.withdraw 30
 14:     assert_equal(70, a.balance)
 15:   end
 16: 
 17:   def test2
 18:     a = Account.new
 19:     a.deposit 50
 20:     assert_exception(RuntimeError) {
 21:       a.withdraw 80
 22:     }
 23:     assert_equal(50, a.balance)
 24:   end
 25: 
 26:   def test3
 27:     a = Account.new
 28:     a.deposit 100
 29:     assert_exception(ArgumentError) {
 30:       a.deposit -50
 31:     }
 32:     assert_equal(100, a.balance)
 33:   end
 34: end
 35: 
 36: RUNIT::CUI::TestRunner.run(AccountTest.suite)

テスト項目として,残高を超えた引き出しとマイナスの預け入れを入れた。例外が発生するかをテストするときは,assert_exceptionメソッドを使う。

実行結果;
AccountTest#test1 .
AccountTest#test2 F.
AccountTest#test3 F.
Time: 0.024356
FAILURES!!!
Test Results:
 Run: 3/3(3 asserts) Failures: 2 Errors: 0
Failures: 2
sample-1.2-test.rb:20:in `test2'(AccountTest): expected:<RuntimeError> 
    but was:<NO EXCEPTION RAISED> (RUNIT::AssertionFailedError)
        from sample-1.2-test.rb:36
sample-1.2-test.rb:29:in `test3'(AccountTest): expected:<ArgumentError> 
    but was:<NO EXCEPTION RAISED> (RUNIT::AssertionFailedError)
        from sample-1.2-test.rb:36

例外が発生しなかったためテストに通らなかった。このテストに通るようにAccountクラスを修正する。

sample-1.2.rb
  5: class Account
  6:   def initialize
  7:     @balance = 0
  8:   end
  9:   def deposit(amount)
 10:     if amount >= 0
 11:       @balance += amount
 12:     else
 13:       fail ArgumentError
 14:     end
 15:   end
 16:   def withdraw(amount)
 17:     if @balance >= amount
 18:       @balance -= amount
 19:     else
 20:       fail RuntimeError, "balance shortage"
 21:     end
 22:   end
 23:   attr_reader :balance
 24: end

再びテストしてみます。

実行結果;
AccountTest#test1 .
AccountTest#test2 .
AccountTest#test3 .
Time: 0.023123
OK (3/3 tests  5 asserts)

今度は通りました。このように,テストを先に修正するようにすると,安全に手を入れることができます。同時に,実際の使われ方も分かるため,設計もスムーズに進めることができます。

リファレンス

runit/testcase