テストコードによる動作確認

自分の書いたコードが、思った通りの動きをしているかどうか...。正直、これまではマウスクリックテストだった。つまり、毎回手動による動作確認をしていたのだ。

  1. サーバーを立ち上げ、ブラウザでページを開き、マウスやキーボードで操作して動作を確認する。
  2. エラーが発生したり、予想外の結果が返ってきた時には、ビューにその時点の変数の内容等を表示するコードを挿入して、実行状況を確認する。
  3. コードを修正したら、再び1の手順に戻り、正常に動作するまでこれを繰り返す。

原始的だけど、余計な知識は不要で、直感的に誰にでも出来る一番分かり易い方法だった。コードに自信が無い時でもとりあえず動作させてみて、その動きを見ながら次第に目的の動作に近づけていくという手順になることが多かった。Rubyスクリプト言語であり、ビルドなしで即実行可能ということもあり、この方法で、規模の小さなサンプルプロジェクトレベルでは比較的良いリズムで開発できた。
ところが、ちゃんと使える実用的なアプリケーションを作ろうと考えだすと、モデルやコントローラーの数は増え、必要な機能も多くなりがち。すると、これまでマウスクリックテストで頑張ってきたが、そのテスト方法に限界を感じ始める...。

  • 機能が複雑になると、準備するテストデーターも複雑になり、ページも複雑になり、マウスクリックする回数も増え、同じテストを繰り返すにも時間がかかる。だんだん同じことの繰り返しが面倒になってきた。
  • 手動では、一度正常な動作を確認できると、その後一旦そのテストのことは気にしなくなってしまう。ところが、別の機能を追加・修正した時に、副作用で以前の動作が正常に完了しなくなる場合が多い。でも、そのことに気付くのはだいぶ後になってから。(最悪、気付かないことも...。)これでは、何が原因でその不具合が発生したのか、その調査に時間がかかってしまう。

そんな状況を解決するために、Railsにはテストコードによって動作確認をする環境が用意されている。script/generateで生成されたモデルやコントローラーにはテストコードをすぐにでも書けるようにディレクトリやファイルが同時に準備されている。マウスクリックテストに限界を感じ始めた今、そろそろちゃんと勉強しておかなければ...。

環境

モデルのテスト

モデルのテストコードは、test/unitフォルダ以下に準備されている。自動生成されたコードを確認してみると、以下のようになっている。

# test/unit/slip_test.rb

require File.dirname(__FILE__) + '/../test_helper'

class SlipTest < ActiveSupport::TestCase
  # Replace this with your real tests.
  def test_truth
    assert true
  end
end
いきなりテストを実行してみる
  • 上記テストコードが用意されていることから、いきなりテストを実行してみた。テスト結果は以下のように表示された。
$ rake test:units
(in /Users/zari/railsapp/test_slip202)
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb" "test/unit/slip_test.rb" 
Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader
Started
.
Finished in 0.091481 seconds.

1 tests, 1 assertions, 0 failures, 0 errors
  • もしassert truと故意に間違ったコードを追記してみると...
def test_truth
  assert true
  assert tru
end
  • エラー箇所とその内容が表示され、「1 tests, 1 assertions, 0 failures, 1 errors」と報告される。
$ rake test:units
(in /Users/zari/railsapp/test_slip202)
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb" "test/unit/slip_test.rb" 
Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader
Started
E
Finished in 0.160339 seconds.

  1) Error:
test_truth(SlipTest):
NameError: undefined local variable or method `tru' for #
    /Library/Ruby/Gems/1.8/gems/actionpack-2.0.2/lib/action_controller/test_process.rb:464:in `method_missing'
    ./test/unit/slip_test.rb:7:in `test_truth'
    /Library/Ruby/Gems/1.8/gems/activesupport-2.0.2/lib/active_support/testing/default.rb:7:in `run'

1 tests, 1 assertions, 0 failures, 1 errors
rake aborted!
Command failed with status (1): [/System/Library/Frameworks/Ruby.framework/...]

(See full trace by running task with --trace)
  • もしassert falseと追記してみると...
def test_truth
  assert true
  assert false
end
  • assertの結果がfalseになっている箇所が表示され、「1 tests, 2 assertions, 1 failures, 0 errors」と報告される。
$ rake test:units
(in /Users/zari/railsapp/test_slip202)
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb" "test/unit/slip_test.rb" 
Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader
Started
F
Finished in 0.111133 seconds.

  1) Failure:
test_truth(SlipTest)
    [./test/unit/slip_test.rb:7:in `test_truth'
     /Library/Ruby/Gems/1.8/gems/activesupport-2.0.2/lib/active_support/testing/default.rb:7:in `run']:
 is not true.

1 tests, 2 assertions, 1 failures, 0 errors
rake aborted!
Command failed with status (1): [/System/Library/Frameworks/Ruby.framework/...]

(See full trace by running task with --trace)
何が確認できるか?
  • テストコードはassertメソッドによって、成功か失敗を判定しているようだ。assertメソッドに続くRuby式の結果がtrueであれば成功、falseであれば失敗と判定される。
  • ステータスコード「1 tests, 2 assertions, 1 failures, 0 errors」について
    • 1 testは、def test_truth等のテストコード定義の数を数えている。
    • 2 assertionsは、実行したassertメソッドの個数を数えている。
    • 1 failuresは、assertメソッドがfalseを受け取った個数を数えている。(テストに失敗した数)
    • 0 errorsは、テスト中に発生したエラーの個数を数えている。
    • つまり「0 failures, 0 errors」と報告されていれば、テストは成功しているということになる。一つでもfailuresやerrorsがあれば失敗だ。

最初から用意されていたdef test_truthのassert trueによって、「1 tests, 1 assertions, 0 failures, 0 errors」が報告されるということは、当り前のことで意味が無いように思える。しかし、当り前のことが成功することによって、テスト環境に問題ないことが保証されると理解した。もし、この状態で「0 failures, 0 errors」と報告されなければ、テスト環境に何らかの問題が発生していると思う。

テストを自分で書く

テスト環境が正常であることが確認できたので、slipモデルの検証がちゃんと機能しているか、以下のように書いてテストしてみた。

require File.dirname(__FILE__) + '/../test_helper'

class SlipTest < ActiveSupport::TestCase
  # Replace this with your real tests.
  def test_truth
    assert true
  end

  def test_invalid_with_empty_attributes
    slip = Slip.new
    journals = slip.make_journals("j123456789"=>{})
    assert !slip.valid?
    assert slip.errors.invalid?(:number)
    assert slip.errors.invalid?(:executed_on)
    assert slip.errors.invalid?(:total_yen)
    assert slip.errors.invalid?(:base)
  end
  
  def test_invalid_with_not_equal_total_yen
    slip = Slip.new(:number=>"1", :executed_on=>"2/1", :total_yen=>"2000")
    journals = slip.make_journals("j123456781"=>{:comment=>"test", :yen=>"1000"})
    assert !slip.valid?
    assert_equal "Total yenが明細の合計と一致していません。", slip.errors.on(:total_yen)
  end
end

実行してみると、以下の結果が表示された。問題なさそうだ。

$ rake test:units
(in /Users/bebe/railsapp/test_slip202)
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb" "test/unit/journal_test.rb" "test/unit/slip_test.rb" 
Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader
Started
....
Finished in 0.174438 seconds.

4 tests, 9 assertions, 0 failures, 0 errors
  • テストは「test_...」で始まるように命名しておく。そのように命名したテストが、rake testで実行されるテストコードになるようだ。
  • もし、一時的にテストしたくない場合、「_test_...」のように適当に命名規約から外れるようにしておけば、そのテストは実行されない。