テストを書いてからコードを実装してみる

これまでテストした中で、いくつかの動作は、現状のままでは問題があることも分かってきた。その一つは、伝票の明細行を削除するところ。調子よく削除していくと、最後の1件も削除できてしまう。そして、伝票には明細行が無い状態になり、挿入する方法も無いことに気付き、途方に暮れる...。(挿入・削除・コピーのリンクが、明細行の中にあるので。)
この問題に対して暫定的に、行削除の結果、明細行が0件の場合は、自動的に1行追加するようにして回避することにしてみた。そして今回は、コードを修正する前に、目指す状態のテストを書いてみた。

まずテスト

# テスト: test/functional/journals_controller_test.rb
require File.dirname(__FILE__) + '/../test_helper'

class JournalsControllerTest < ActionController::TestCase
  # 2行以上の明細状態から削除
  def test_should_delete_row
    xml_http_request :post, :delete, 
        {:index  =>'j123456789', 
         :journal=>{"j123456789"=>{:comment=>"test", :yen=>"1000", :index=>"j123456789", :position=>1}, 
                    "j123456790"=>{:comment=>"test", :yen=>"1000", :index=>"j123456790", :position=>2}
                    }
         }
    assert_response :success
    assert_select_rjs :remove, "j123456789"
    assert_select_rjs "journals_footer" do
      assert_select "tbody>tr" do
        assert_select "th", 3
        assert_select "th[align=right][colspan=2]:nth-child(2)", 1
        assert_select "th input[value=1,000]", 1
      end
    end
  end

  # 最後の1行を削除
  def test_should_delete_last_row
    xml_http_request :post, :delete, 
        {:index  =>'j123456789', 
         :journal=>{"j123456789"=>{:comment=>"test", :yen=>"1000", :index=>"j123456789", :position=>1}
                    }
         }
    assert_response :success
    assert_select_rjs :remove, "j123456789"
    assert_select_rjs :insert, :before, "journals_footer" do ######32行目
      assert_select "tbody>tr" do
        assert_select 'th', 2
        assert_select 'td', 2
        assert_select 'td input[value]', false
      end
    end
    assert_select_rjs :replace, "journals_footer" do
      assert_select "tbody>tr" do
        assert_select 'th', 3
        assert_select "th[align=right][colspan=2]:nth-child(2)", 1
        assert_select "th input[value=0]", 1
      end
    end
  end
end
  • 当然のことながら、まだ実装していないので、このテストはFailure、失敗する。
1) Failure:
test_should_delete_last_row(JournalsControllerTest)
[/Library/Ruby/Gems/1.8/gems/actionpack-2.0.2/lib/action_controller/assertions/selector_assertions.rb:467:in `assert_select_rjs'
./test/functional/journals_controller_test.rb:32:in `test_should_delete_last_row'
/Library/Ruby/Gems/1.8/gems/activesupport-2.0.2/lib/active_support/testing/default.rb:7:in `run']:
No RJS statement that replaces or inserts HTML content.

2 tests, 13 assertions, 1 failures, 0 errors
  • /test/functional/journals_controller_test.rbの32行目で発生しているようだ。(test_should_delete_last_rowメソッドの以下の部分)
assert_select_rjs :insert, :before, "journals_footer" do ######32行目

コードの実装

  • 自動的に1行追加する機能がないのが原因なので、早速、実装してみる。(オレンジ色の部分)
class JournalsController < ApplicationController
  before_filter :for_editing_rows, :only=>[:insert, :copy, :delete]

...(中略)...

  def delete
    @effect_item = @slip.delete_journal(params[:index])
    render :update do |page|
      highlight_row(page, @effect_item.index, :duration=>2, :startcolor=>"'#666666'")
      page.delay(1) do
        page.remove params[:index]
        numbering_row(page, @effect_item.position - 1)
        if @slip.editing_journals.empty?
          @effect_item = @slip.insert_journal
          page.insert_html :before, 'journals_footer', render(:partial=>'journals/form', :object=>@effect_item)
          highlight_row(page, @effect_item.index, :duration=>2)
          numbering_row(page, @effect_item.position - 1)
        end
        page.replace 'journals_footer', :partial=>'journals/footer'
      end
    end  
  end

private

  # 挿入、削除、コピーの前処理
  def for_editing_rows
    @slip = Slip.new #(params[:slip])
    @slip.make_journals(params[:journal])
  end

  def render_insert(position, id)
    render :update do |page|
      page.insert_html position, id, render(:partial=>'journals/form', :object=>@effect_item)
      highlight_row(page, @effect_item.index, :duration=>2)
      numbering_row(page, @effect_item.position - 1)
      page.replace 'journals_footer', :partial=>'journals/footer'
    end
  end
end
  • すると、今度はErrorが発生。
    • 引数の個数が間違っていると。(引数1のところ、0になっていると読むようだ。)
    • 「@slip.insert_journal」の部分が問題箇所。(app/controllers/journals_controller.rb:117)
1) Error:
test_should_delete_last_row(JournalsControllerTest):
ArgumentError: wrong number of arguments (0 for 1)
/Users/bebe/railsapp/test_slip202/app/controllers/journals_controller.rb:117:in `insert_journal'
...(中略)...

22 tests, 190 assertions, 0 failures, 1 errors
  • 「def insert_journal(params_index = nil)」と修正したが、またしてもErrorが発生。
    • nilに対するメソッド呼び出しが問題のようだ。(nil.positionを実行しようとしている。)
    • 「current_journal.position」の部分が問題箇所。(app/models/slip.rb:74)
1) Error:
test_should_delete_last_row(JournalsControllerTest):
NoMethodError: You have a nil object when you didn't expect it!
The error occurred while evaluating nil.position
/Users/bebe/railsapp/test_slip202/app/models/slip.rb:74:in `insert_journal'
...(中略)...

22 tests, 190 assertions, 0 failures, 1 errors
  • 上記2つのエラーで修正したのはオレンジ色の部分。以下のようになった。
# モデル: app/models/slip.rb
...(中略)...
  def insert_journal(params_index = nil)
    current_journal = journal_at(params_index)
    current_position = (current_journal.position rescue 1) ######74行目
    insert_index = current_position - 1
    insert_journal = Journal.new(:position=>current_position)
    @editing_journals.insert(insert_index, insert_journal)
    insert_journal
  end
...(中略)...

テストが成功

以上の修正で、テストは通り、結果はグリーンになった。そして、やはり最後はマウスクリックテストだ。最後の1行を削除してみると...削除した後、すぐに1行追加された!

現実

以上の経過は、経験不足の自分が思い描く空想のテスト駆動開発だ。上記のテスト、コード実装は実際にやってみたことだし、最終的なコードは上記の通りになっているが、現実の途中経過は、こんなにシンプルな手順では終わらなかった...。以下、自分のメモ。

  • 先にテストを書くが、そのテストが正しいのかどうか不安。実装中に何度も書き直した。
    • →テストに慣れれば、もう少し自信を持てるし、的確に書けるかもしれない。
  • 上記に関連して、不慣れな現状では、テストは極力シンプルに書く。代入や繰り返し、条件分岐が多くなると、テストをテストしたい気分になってくる。
    • →パラメーターに値を直接書いてしまっても、自分が分かり易ければ、それでもいいと割り切ってしまう。
    • →おそらく条件分岐なんか必要ない。テストを分ければいい。
    • →以下のようにループするよりも...
[:number, :executed_on, :total_yen, :base].each do |field|
  assert slip.errors.invalid?(field)
end
    • →assertを何度も書いた方が見やすい、直感的。
assert slip.errors.invalid?(:number)
assert slip.errors.invalid?(:executed_on)
assert slip.errors.invalid?(:total_yen)
assert slip.errors.invalid?(:base)
  • autotestはコード変更の差分をチェックして、必要な部分のテストだけ実施してくれるようだが、autotestが通っても、改めてすべてのテストを実行すると失敗していることがあった。
    • →メソッドの割り振りミス?(JournalコントローラーからSlipモデルを呼び出しているから?)
    • →常にすべてのテストを実施するように変更してしまいたい。(autotestの設定ファイルで出来るのだろうか?)
  • モデルのテストも必要だったかもしれない...。コード実装中に新たなテストの必要性を感じたら、その都度追加した方が良さそう。
# テスト: test/unit/slip_test.rb
require File.dirname(__FILE__) + '/../test_helper'

class SlipTest < ActiveSupport::TestCase
...(中略)...
  def test_insert_journal_in_nil
    slip = Slip.new(:number=>"1", :executed_on=>"2/1", :total_yen=>"1000")
    slip.make_journals("j123456789"=>{:comment=>"test", :yen=>"1000", :position=>1, :index=>"j123456789"})    
    slip.delete_journal("j123456789")
    assert_difference "slip.editing_journals.size" do
      insert_journal = slip.insert_journal
      assert_nil insert_journal.comment
      assert_nil insert_journal.yen
    end
  end


もう少しテストの経験が必要だ...。とりあえず上記の方針で、続けてみる。