ひとつのテストメソッドにはひとつのassert文、そしてEmacsサポート

Jay Fields' Thoughts: Testing: One assertion per test
Jay Fields' Thoughts: Testing: Inline Setup

彼の例は電話番号クラスのテスト。

class PhoneNumberTest < Test::Unit::TestCase
  def test_initialize
    number = PhoneNumber.new "212", "555", "1212"
    assert_equal "212", number.area_code
    assert_equal "555", number.exchange
    assert_equal "1212", number.station
  end
end

こんなテストを書いてると、もし、PhoneNumber#initializeにバグがあったら最初のassertionしか失敗しないのでよろしくない。

class PhoneNumberTest < Test::Unit::TestCase
  def test_area_code_is_initialized_correctly
    number = PhoneNumber.new "212", "555", "1212"
    assert_equal "212", number.area_code
  end

  def test_exchage_is_initialized_correctly
    number = PhoneNumber.new "212", "555", "1212"
    assert_equal "555", number.exchange
  end

  def test_station_is_initialized_correctly
    number = PhoneNumber.new "212", "555", "1212"
    assert_equal "1212", number.station
  end
end

面倒だが、assertionごとにテストメソッドを分けるほうがいろいろと利点がある。

  • initializeがbugってるときは、3つともテストが失敗してくれる。
  • 振舞いを表す正しい名前がつけられる。名前重要。
  • 高保守性とDRY・高抽象性・オブジェクト指向は必ずしも一致しない。

テストの保守性とは、テストの意図を理解するのに必要な情報がすべて含まれていること。 コケた場合、修復しやすいこと。

setupイラネ。

  • たしかにDRYにはなるけど、テストメソッドのみだとテストは完結しない。
  • setupを実行する必要がないのに実行してしまうことがある。
  • テストが長くなると、最悪、setupに気付かないでテストを書いてしまうことがある。
  • 長いsetupが必要になってきたらリファクタリングすべきという警報だ。
  • one assertion per testを実践してたらsetupなどそもそも不要。

setupは不要だが、teardownについては述べていないようだ。後片付だからsetupと違ってそこにテストを理解する情報がないってことなのかな。

要はテストを書くときはガンガンコピペプログラミングしろってこと!!setupなんか使わずにテストメソッドで初期化を含めテストに必要な情報を書くべき。だって、テストメソッド「だけ」見ていればテストの意図がわかるからだ。



彼の考え方はライブラリとして実装している。
gem install dust
してみよう。

俺もテストを書いてて抽象化しすぎてて後で読んだらかえってわかりにくかったりした。この考え方で光明が見えてきた気がする。俺が公開しているフリーソフトウェアのテストを見直してみようと思う。さて、実践だ。DRYがかえってテストの保守性を下げてしまっているのは、ある意味皮肉だ。




余談だが彼も俺達のxmpfilterを気に入ってくれてるようだ^^

直前のテストメソッドを貼り付けるEmacs Lisp

このone assertion per test方式を実践するには、「カーソル直前のメソッドをコピーする」エディタマクロが必要なのでquick hackでこしらえてみた。彼は使っているのかな。
ついでにテストメソッド名を修正しやすいようにカーソルを移動させておく。

(defun ruby-duplicate-previous-test-method ()
  "Duplicate previous test method and arrange the point."
  (interactive)
  ;; assumes that indent("def") == indent("end")
  (let (s e)
    (save-excursion
      (when (re-search-backward "^\\( +\\)end$" nil t)
        (forward-line 1)
        (setq e (point))
        (when (re-search-backward (concat "^" (match-string 1) "def ") nil t)
          (setq s (point-at-bol)))))
    (when (and s e)
      (delete-region (point-at-bol) (point))
      (save-excursion (insert (buffer-substring s e) "\n"))
      (unless (search-forward "__" (point-at-eol) t)
        (search-forward "test_" (point-at-eol) t)))))

でっかいファイルの処理はどうするかという問題

トラックバックが来たので返事。

Test::Unit::TestCase#setupの使い方について - Rubyist til i die - Rubyist

でっかいファイルを処理して情報を取り出すようなクラスを書くと、個々のメソッドのテスト にいちいちヒアドキュメントとか書いていられないので、setupで一発ドカン とでっかいファイ ルをTempfileオブジェクトにputsしておいて使ったりし ますが、そういうのはマズいでしょう か?

俺ならば TextArrayFormat を作って、テストに必要な情報をそのファイルに並べておくかな。Railsみたいにfixtureを外に出す感覚。ヒアドキュメントが何度も出てくると醜いのでなんとかできないかと考えて思い付いた。

なんかこう、でっかいファイルは一回書いておいて後のテストでは使いまわし、っていうのが いいのだと思うけれど、どうやって実現すればいいのかいまいち分からない。ソースコードに 別途含めておくのがいいのかな。みなさんどうしてますか。

テストメソッドの外に書いて定数だかクラス変数だかに入れておく。Railsでもfixtureをテストメソッドの外に書いては複数のテストメソッドで共有している。

class FooTest < Test::Unit::TestCase
  @@large_data = LargeData.new("arg")

  def test_xxxx
    # ...
  end
end