そのコードはテストされているか?

テストコードを書いていると、ふと疑問を感じて心配になることがある。

  • 果たして、このテストコードで漏れなく動作確認できているのだろうか?
  • テストが漏れているところが、どこかに無いだろうか?

特にテストを書くことに慣れていない現状では、何処でどんなテストを書いておくべきかも手探りだ。そんな時rcovが勇気づけてくれた。

利用環境

インストール

  • インストールはいつもながら、とても簡単。
$ sudo gem install rcov

使い方

  • 開発中のRailsプロジェクトのルートで以下のコマンドを実行してみた。
    • -x Library/Ruby/Gemsオプションは、Library/Ruby/Gems/以下のコードに対するcoverageを除外してくれる。
    • --railsオプションは、config/, environment/, vendor/以下のコードに対するcoverageを除外してくれる。
    • test/**/*_test.rbによって、testフォルダ以下のoooo_test.rbという書式のファイルすべてが調査対象のテストになる。
$ cd ~/railsapp/test_slip202
$ rcov -x Library/Ruby/Gems --rails test/**/*_test.rb
  • コマンドが完了すると、test_slip202/coverageフォルダ以下にHTMLファイルが生成されている。index.htmlを開いてみる。

  • 一番下の行app/models/slip.rbのCode coverageを見ると「61.2%」と表示されている。つまり、実行コードのうち「61.2%」は何らかのテストがされているが、残りの「38.8%」は全くテストされていないということだ。
  • app/models/slip.rbのリンクをクリックすると、コードの詳細を確認できた。

  • テストされていないコードについては、赤で示されている。確認してみると、挿入・削除・コピー関連のメソッドが赤くなっていた。確かにテストコードをまだ書いていなかった...。
  • 無駄が多い気がするが、以下のようにテストを追加してみた。
# テスト: test/units/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

  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
  
  def test_copy_journal
    slip = Slip.new(:number=>"1", :executed_on=>"2/1", :total_yen=>"1000")
    journals = slip.make_journals("j123456789"=>{:comment=>"test", :yen=>"1000", :position=>1, :index=>"j123456789"})
    journals_count = slip.editing_journals.size
    original_journal = journals[0]
    copy_journal     = slip.copy_journal("j123456789")

    assert_equal slip.editing_journals.size, journals_count + 1
    assert_equal original_journal.comment, copy_journal.comment
    assert_equal original_journal.yen,     copy_journal.yen
    assert_equal     slip.editing_journals[0].index, "j123456789"
    assert_not_equal slip.editing_journals[1].index, "j123456789"
  end

  def test_insert_journal
    slip = Slip.new(:number=>"1", :executed_on=>"2/1", :total_yen=>"1000")
    journals = slip.make_journals("j123456789"=>{:comment=>"test", :yen=>"1000", :position=>1, :index=>"j123456789"})    
    journals_count = slip.editing_journals.size
    original_journal = journals[0]
    insert_journal   = slip.insert_journal("j123456789")

    assert_equal slip.editing_journals.size, journals_count + 1
    assert_nil insert_journal.comment
    assert_nil insert_journal.yen
    assert_not_equal slip.editing_journals[0].index, "j123456789"
    assert_equal     slip.editing_journals[1].index, "j123456789"
  end

  def test_delete_journal
    slip = Slip.new(:number=>"1", :executed_on=>"2/1", :total_yen=>"3000")
    journals = slip.make_journals("j123456781"=>{:comment=>"test", :yen=>"1000", :position=>1, :index=>"j123456781"}, 
                                  "j123456782"=>{:comment=>"test", :yen=>"2000", :position=>2, :index=>"j123456782"})    
    journals_count = slip.editing_journals.size
    delete_journal = slip.delete_journal("j123456782")

    assert_equal slip.editing_journals.size, journals_count - 1
    assert_nil slip.journal_at("j123456782")
  end
end
  • 再び、rcovを実行してみる。
$ rcov -x Library/Ruby/Gems --rails test/**/*_test.rb


ブラウザでcoverage/index.htmlを再読み込みしてみると...app/models/slip.rbのCode coverageは100%になった。ひとまず安心できた!

コントローラーの追加テスト

  • 同じようにコントローラーのテストにも漏れがあったので、以下のように追記してみた。
    • create、updateが検証エラーで登録できなかった場合のテストを追加した。(このテストについては全く忘れていたので、rcovで発見できて良かった。)
    • 明細行の挿入・削除・コピーのテストを追加した。
require File.dirname(__FILE__) + '/../test_helper'

class SlipsControllerTest < ActionController::TestCase
...(中略)...
  def test_should_not_create_slip
    post :create, {:slip   =>{:number=>'3', :executed_on=>'2/20', :total_yen=>'3000'}, 
                   :journal=>{"1"=>{:yen=>"1000", :index=>"j943792543", :position=>"1", :comment=>"test3"}}
                  }
    assert_response :success
    assert_template 'new'
  end

  def test_should_not_update_slip
    put :update, {:id     =>'1', 
                  :slip   =>{:number=>'1', :executed_on=>'2/14', :total_yen=>'10,000'}, 
                  :journal=>{"1"=>{:yen=>"1,000", :index=>"j943792544", :position=>"1", :comment=>"test"}}
                 }
    assert_response :success
    assert_template 'edit'
  end

  def _test_should_insert_row
    xhr :post, :insert_row, 
        {:index  =>'1', 
         :slip   =>{:number=>"1", :executed_on=>"2/1", :total_yen=>"1000"}, 
         :journal=>{"1"=>{:comment=>"test", :yen=>"1000", :position=>1, :index=>"1"}}
        }
    assert_response :success
    assert_select 'tbody[id^=j9] tr' do
      assert_select 'th', 2
      assert_select 'td', 2
    end
    assert_select 'tbody#journals_footer tr' do
      assert_select 'th', 3
      assert_select 'th input[value=1,000]', 1
    end
  end

  def _test_should_copy_row
    xhr :post, :copy_row, 
        {:index  =>'1', 
         :slip   =>{:number=>"1", :executed_on=>"2/1", :total_yen=>"1000"}, 
         :journal=>{"1"=>{:comment=>"test", :yen=>"1000", :position=>1, :index=>"1"}}
        }
    assert_response :success
    assert_select 'tbody[id^=j9] tr' do
      assert_select 'th', 2
      assert_select 'td input[value=test]', 1
      assert_select 'td input[value=1,000]', 1
    end
    assert_select 'tbody#journals_footer tr' do
      assert_select 'th', 3
      assert_select 'th input[value=2,000]', 1
    end
  end

  def _test_should_delete_row
    xhr :post, :delete_row, 
        {:index  =>'1', 
         :slip   =>{:number=>"1", :executed_on=>"2/1", :total_yen=>"1000"}, 
         :journal=>{"1"=>{:comment=>"test", :yen=>"1000", :position=>1, :index=>"1"}, 
                    "2"=>{:comment=>"",     :yen=>"",     :position=>2, :index=>"2"}}
        }
    assert_response :success
    assert_select 'tbody#journals_footer tr' do
      assert_select 'th', 3
      assert_select 'th input[value=0]', 1
    end
  end
end


うーん...相変わらず悩みながらのテストだ...。

  • 果たして、上記のテストコードでちゃんとテストできているのだろうか?
  • rcovはテストコードによって一度でも実行された部分はOKとしてくれるようだが、テストでどんなチェックをしておくべきかは作っている人間のみが知っている...。
  • Code coverageが100%だからといって、単純に安心していてはいけないことに気付いた。
  • もっともっとテストの達人が書いたコードを勉強しておく必要がある。

参考ページ

以下のページがたいへん参考になりました。感謝です!

coverage
保証範囲とか、普及率、取扱い範囲などの意味を持っているようだ。