2008年03月27日 03:00 [Edit]

「同じコード」の同じって何さ - TAPのススメ

問題は、この「同じコード」の定義。

「誰が書いても同じコード」は大事なことなのか - ひがやすを blog
でも、「誰が書いても同じコード」にするってのは、そもそも無理だと思うんだよね。そうやって、わざわざドキュメントをたくさん書かせても、めためたなコードを書くやつはいて、総合テストするときに、現場は燃え上がるもの。ある程度の規模以上のプロジェクトなら、どこでもそんな感じじゃないかと思います。

同じ「書き方」をしなければならないのか?

結果が「同じ」ならいいのか?

もし後者だとしたら、実は

重要なのは、「誰でもメンテナンスできるコード」にすること。そのために、コーディング規約は、きちんと決めてみんなで守る、それ以上は、がちがちに縛る必要はない。

すら必要ありません。なぜなら、最悪コードは書き直せばいいんですから。

しかし、「そのコードが何をすべきか」がわかっていないと、たとえソースがあっても無力な場合がほとんど。奇麗なソースですらそうで、ましてやスパゲッティコードともなれば。

その意味で、実はコーディング規約より、メンテナブルなコードよりも役に立つのが、テスト。要はテストをパスしてしまえばどうコードしても構わない、というのがTDD = Test Driven Development =テスト駆動開発の考え方のベースとなっています。

のですが、TDD、Perlを除いていまいち普及してません。それがなぜかといえば、テストがあまりにも面倒--だと思われているからです。これを見れば、面倒だと思われるのも当然だと思います。

「TDDer養成ギブス」始めました - t-wadaの日記
public void testDefaultStackSizeShouldBeZero throws Exception {
  assertEquals("作成直後のスタックのサイズは0であること", 0, stack.size());
}
Test::Unit - Rubyリファレンスマニュアル
require 'test/unit'
require 'foo'

class TC_Foo < Test::Unit::TestCase
  def setup
    @obj = Foo.new
  end

  # def teardown
  # end

  def test_foo
    assert_equal("foo", @obj.foo)
  end
  def test_bar
    assert_equal("bar", @obj.bar)
  end
end

たかだかテスト一個のために、関数作れだのメソッド作れだのって上司に言われたら、私だってうんこ入りのタッパーを上司に送りつけたくなりますよ。私は実際にそうする度胸まではないので、assert_unko()を実装してmailで送りつける程度にとめとくとも思いますが。

そんなあなたにお勧めなのが、Test Anything Protocol、略してTAPです。略称のごとく軽やかです。元々Perlで使われていましたが、あまりにシンプルなので言語を問いません。なにしろテストプログラムは、標準出力にこんな風に出力するだけでいいのですから。

Test Anything Protocol - Wikipedia, the free encyclopedia
1..N
ok 1 Description # Directive
# Diagnostic
....
ok 47 Description
ok 48 Description
more tests....

実際にrubyでFizzBuzzをテストしてみましょう。

% ruby fizzbuzz.t
1..17
ok # 1
ok # 2
ok # 3
ok # 4
ok # 5
ok # 6
ok # 7
ok # 8
ok # 9
ok # 10
ok # 11
ok # 12
ok # 13
ok # 14
ok # 15
# got:      
# expected: FizzBuzz
not ok # 16
# got:      
# expected: 16

これでも要点はわかりますし、

% ruby fizzbuzz.t | grep ^not
not ok # 0
not ok # 16

で問題を発見できるという点もぐーですが、perlがインストールされていれば、華麗にテストを実行してくれるプログラム、proveも漏れなくついてきます。これ、Perl以外のプログラムでも、TAPに準拠していればテストしてくれるのですよ。

% prove fizzbuzz.t
fizzbuzz....FAILED tests 16-17                                               
        Failed 2/17 tests, 88.24% okay
Failed Test Stat Wstat Total Fail  List of Failed
-------------------------------------------------------------------------------
fizzbuzz.t                17    2  16-17
Failed 1/1 test scripts. 2/17 subtests failed.
Files=1, Tests=17,  0 wallclock secs ( 0.00 cusr +  0.01 csys =  0.01 CPU)
Failed 1/1 test programs. 2/17 subtests failed.

では、実際のテストプログラムfizzbuzz.tと、テストプログラムを駆動するためのプログラムtap.rbを見てみましょう。 fizzbuzz.t

#!/usr/local/bin/ruby

require 'tap.rb'
require 'fizzbuzz.rb'

test 17
is fizzbuzz(1),   1,           1
is fizzbuzz(2),   2,           2
is fizzbuzz(3),  'Fizz',       3
is fizzbuzz(4),   4,           4
is fizzbuzz(5),  'Buzz',       5
is fizzbuzz(6),  'Fizz',       6
is fizzbuzz(7),   7,           7
is fizzbuzz(8),   8,           8
is fizzbuzz(9),  'Fizz',       9
is fizzbuzz(10), 'Buzz',      10
is fizzbuzz(11),  11,         11
is fizzbuzz(12),  'Fizz',     12
is fizzbuzz(13),  13,         13
is fizzbuzz(14),  14,         14
is fizzbuzz(15), 'FizzBuzz',  15

is fizzbuzz(0),  'FizzBuzz',   0
is fizzbuzz(16), 16,          16
tap.rb
def test(ntests=0)
    puts "1..#{ntests}" if ntests > 0
end

def is(got, expected, message=nil)
    print 'not ' if expected != got
    print 'ok'
    print " # #{message}" if message
    print "\n";
    if expected != got
       puts "# got:      #{got}"
       puts "# expected: #{expected}"
    end
end

tap.rb自体もFizzBuzz並みに簡単な点にご留意ください。

で、(わざとらしく)テストにこけたFizzBuzzが実装されているfizzbuzz.rbを見てみましょう。

def fizzbuzz(n)
    return 1 if n == 1
    return 2 if n == 2
    return 'Fizz' if n == 3
    return 4 if n == 4
    return 'Buzz' if n == 5
    return 'Fizz' if n == 6
    return 7 if n == 7
    return 8 if n == 8
    return 'Fizz' if n == 9
    return 'Buzz' if n == 10
    return 11 if n == 11
    return 'Fizz' if n == 12
    return 13 if n == 13
    return 14 if n == 14
    return 'FizzBuzz' if n == 15
end

こんなの部下が書いたら今度は部下にうんこ入(ry

というのはおいといて、これを

def fizzbuzz(n)
    fz =   if n % 3 == 0 then 'Fizz' else '' end
    fz +=  if n % 5 == 0 then 'Buzz' else '' end
    if fz == '' then n else fz end
end

とでも差し替えて見ましょう。今度はどうでしょうか?

% prove fizzbuzz.t
fizzbuzz....ok                                                               
All tests successful.
Files=1, Tests=17,  0 wallclock secs ( 0.00 cusr +  0.01 csys =  0.01 CPU)

このTAP、Wikipediaの説明でも充分なのですが、"Perl Testing"にはさらに詳しく載っています。まだ日本語版はなく、また頭に"Perl"とついてはいるのですが、内容は平易で、お値段もこの手の本としては3,000円を切っているので、PerlプログラマーでなくてもTDDをやろうとする人は目を通しておくべき一冊です。

assertにうんざりしているあなた、TestはもっとSimpleになりうるしそうすべきなのです。

Dan the TAP Dancer


この記事へのトラックバックURL

この記事へのトラックバック
TAP(Test Anything Protocol)とは、テストを簡潔に記述するための書き方(プロトコル)のことです。プロトコルというと難しそうですが、実際はとても簡単。標準出力に、以下のような出力を ...
PHPでTAP(Test Anything Protocol)を使う【webエンジニア日誌】at 2013年10月15日 00:01
Ruby の添付ライブラリ test/unit は、Java のテスト・フレームワークを範にしているようで、煩雑で軽やかさがないのが難点です。なぜ、Perl のテスト・フレームワークに倣わなかったのか、Ruby の不思議の一つだと思っています。id:kogaidan さんが不満を述べるのも、わか
[Ruby]Test::Unit に Test::Tap を被せてみました【Tociyuki::Diary】at 2008年06月11日 23:30
インプレス石橋様より献本御礼。 まるごと Ruby! Vol.1 [インプレスのページ] これはまた思い切ったなあ、というのが第一印象。 「まるごとPerl! Vol.1」や「まるごとJavaScript & Ajax! Vol.1」より、脱初心者度が一段高い。 私には絶好の難易度だったのだけ....
Ruby beyond Rails - 書評 - まるごとRuby!【404 Blog Not Found】at 2008年06月03日 23:03
「誰が書いても同じコード」は大事なことなのか - ひがやすを blog 404 Blog Not Found:「同じコード」の同じって何さ - TAPのススメ メンテナブルなコードよりもテストが重要っておかしくない? - ひがやすを blog TAP言いたいだけやろ、とは言わない。あれ、言っちゃった。
コードを捨てるプログラミング【逆噴はてダ】at 2008年03月29日 14:45
その意味で、実はコーディング規約より、メンテナブルなコードよりも役に立つのが、テスト。要はテストをパスしてしまえばどうコードしても構わない、というのがTDD = Test Driven Development =テスト駆動開発の考え方のベースとなっています。 404 Blog Not Found:「同じ
メンテナブルなコードよりもテストが重要っておかしくない?【ひがやすを blog】at 2008年03月28日 10:48
TAPって意外と知られてないのかな - TokuLog 改め だまってコードを書けよハゲ 404 Blog Not Found:「同じコード」の同じって何さ - TAPのススメ 弾さんの記事見て思い出した。 以前、id:tokuhiromさんに教えてもらったTAPの話ですが、折角なので導入しよーかと。 PHP版HTML
[PHP][Perl][Programming]TAPの話【Unknown::Programming】at 2008年03月27日 15:13
この記事へのコメント
dan先生は『大規模開発』をやった経験があるんでしょうかね?
数百人が毎日うんこをどんどん生産してリビジョンがトイレに行く
回数くらい上がっていくという。悪夢のような。
まぁ、大規模開発を経験してなお「このテスト観」なら大したものです;)
Posted by い at 2008年03月30日 16:16
結局、悪循環は否めないね。
Posted by あ at 2008年03月29日 06:06
aztさん
TAPについては知りませんでしたが、ソースを開いたら、うんこ(修正前のfizzbuzz.rb など)がはいっていた際によくやる手を紹介。

これなら、解読に時間をかけなくても、良いです。
結果が同じになればいいという考え方です。

メソッド単位くらいで、組みなおす。(仮にfizzbuzz_nというメソッドを作成。)
fizzbuzz.t を(is fizzbuzz(1),fizzbuzz_n(1), 1)みたいにしてテスト。
テストがALL OKになれば、入れ替え。
(ただしケース漏れを防ぐのに、場合により、元のメソッドを分割したりすること。)
Posted by PLIer at 2008年03月28日 10:22
no_plan で
Posted by Re 通りスガイ at 2008年03月27日 20:58
簡単なものは簡単に書けるべき、というのは分かるけど、その例のTest::Unitはせいぜい数行の決まり文句が必要という程度だよね。それもエディタでテンプレート化できる類の。
alias is assert_equal と書けばもう大して変わらないんじゃね?

しかも、この記事の前段の例は一個だけでオブジェクトの生成を伴うもの、後段の例は十数個並べた上にstaticな関数のテスト。そんな比較のしかたはないよ。
Posted by 774 at 2008年03月27日 15:21
fooさん

「最悪コードは書き直せばいいんですから」っていうのは解読と修正にかかる時間が、一から書き直すよりも大きい(と判断した)場合でしょう。
Posted by azt at 2008年03月27日 11:34
>なぜなら、最悪コードは書き直せばいいんですから。

さすがスーパー・ハッカーは言うことが違う。100万ステップ超のぐちょぐちょコードでもさらっと解読して全部書きなおしも平気なんだあ。

弾さんのそのスーパーな能力が本当にうらやましいです。
Posted by foo at 2008年03月27日 11:28
test 17 の 17 とかってテストを増やしたり減らしたりするたびに
手で更新しなければ行けないんですか?
Posted by 通りスガイ at 2008年03月27日 10:43
はっきりいってこれを導入するのがめどいです・・・
Posted by gnety at 2008年03月27日 03:51