FizzBuzzでテスト駆動開発をしてみた

http://gihyo.jp/magazine/wdpress/archive/2010/vol56
Web+DB Press vol56で「RSpecを用いたテスト駆動開発」という記事がありました。
いままでTDDは興味あったけどやったことなかったし
せっかくなのでPerlで同じことしてみました。
ほとんどが記事を元にしているため冗長なところもありますがわざとです。
気にしないでください。


TDDの考え方

0. Think(設計)
1. Red(テスト失敗)
2. Green(テスト成功)
3. Refactor(リファクタリング


基本的には1-3を繰り返してコーディングしていくようです。



まずはPerlモジュール雛形作成

module-starter --module=FizzBuzz

こんなのができる

lib/FizzBuzz.pm

package FizzBuzz;

use warnings;
use strict;
use Carp;

use version; our $VERSION = qv('0.0.1');

# Other recommended modules (uncomment to use):
#  use IO::Prompt;
#  use Perl6::Export;
#  use Perl6::Slurp;
#  use Perl6::Say;

# Module implementation here

1; # Magic true value required at end of module
__END__

最初のテスト

「t/01.fizzbuzz.t」というファイルを新規作成して、
以下のように最小限のテスト書く。

use Test::More ;
use FizzBuzz;

my $c = FizzBuzz->new;
is($c->say(1),1,"与えられた数字が1の時、1を返すこと");

done_testing;

テストが通らないことを確認

% perl Build.PL
% perl Build ;perl Build test;                                       
Building FizzBuzz
t/00.load.t ...... 1/1 # Testing FizzBuzz 0.0.1
t/00.load.t ...... ok   
t/01.fizzbuzz.t .. Can't locate object method "new" via package "FizzBuzz" at t/01.fizzbuzz.t line 4.
t/01.fizzbuzz.t .. Dubious, test returned 255 (wstat 65280, 0xff00)
No subtests run 
t/pod.t .......... ok   

Test Summary Report
-------------------
t/01.fizzbuzz.t (Wstat: 65280 Tests: 0 Failed: 0)
  Non-zero exit status: 255
  Parse errors: No plan found in TAP output
Files=3, Tests=2,  0 wallclock secs ( 0.03 usr  0.02 sys +  0.10 cusr  0.02 csys =  0.17 CPU)
Result: FAIL
Failed 1/3 test programs. 0/2 subtests failed.

テスト通るようにコード直す

最小限のnewとsayサブルーチンを書く。
あと無駄な部分はそぎ落としてます。
lib/FizzBuzz.pm

package FizzBuzz;

use warnings;
use strict;
use Carp;
use version; our $VERSION = qv('0.0.1');

# Module implementation here
sub new { bless {} };
sub say {
    my $self = shift;
    1;
}

1;
__END__

テスト通ることを確認

% perl Build ;perl Build test;                                       
Building FizzBuzz
t/00.load.t ...... 1/1 # Testing FizzBuzz 0.0.1
t/00.load.t ...... ok   
t/01.fizzbuzz.t .. ok   
t/pod.t .......... ok   
All tests successful.
Files=3, Tests=3,  0 wallclock secs ( 0.03 usr  0.02 sys +  0.10 cusr  0.02 csys =  0.17 CPU)
Result: PASS

「2」を与えた時のテストを追加

"与えられた数字が2の時、2を返すこと"
t/01.fizzbuzz.t

is($c->say(2),2,"与えられた数字が2の時、2を返すこと");

テストが通らないことを確認

% perl Build ;perl Build test;                                        
Building FizzBuzz
t/00.load.t ...... 1/1 # Testing FizzBuzz 0.0.1
t/00.load.t ...... ok   
t/01.fizzbuzz.t .. 1/? 
#   Failed test '与えられた数字が2の時、2を返すこと'
#   at t/01.fizzbuzz.t line 7.
#          got: '1'
#     expected: '2'
# Looks like you failed 1 test of 2.
t/01.fizzbuzz.t .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/2 subtests 
t/pod.t .......... ok   

Test Summary Report
-------------------
t/01.fizzbuzz.t (Wstat: 256 Tests: 2 Failed: 1)
  Failed test:  2
  Non-zero exit status: 1
Files=3, Tests=4,  0 wallclock secs ( 0.03 usr  0.02 sys +  0.10 cusr  0.02 csys =  0.17 CPU)
Result: FAIL
Failed 1/3 test programs. 1/4 subtests failed.

テスト通るようにコード直す

引数をそのままreturnするだけのコード。

# say
sub say {
    my $self = shift;
    my $n  = shift||0;
    $n;
}

テスト通ることを確認

% perl Build ;perl Build test;                                        
Building FizzBuzz
t/00.load.t ...... 1/1 # Testing FizzBuzz 0.0.1
t/00.load.t ...... ok   
t/01.fizzbuzz.t .. ok   
t/pod.t .......... ok   
All tests successful.
Files=3, Tests=4,  1 wallclock secs ( 0.03 usr  0.02 sys +  0.10 cusr  0.03 csys =  0.18 CPU)
Result: PASS

「3」を与えた時のテストを追加

"与えられた数字が3の時、Fizzを返すこと"
t/01.fizzbuzz.t

s($c->say(3),"Fizz","与えられた数字が3の時、Fizzを返すこと");

テストが通らないことを確認

% perl Build ;perl Build test;                                        
Building FizzBuzz
t/00.load.t ...... 1/1 # Testing FizzBuzz 0.0.1
t/00.load.t ...... ok   
t/01.fizzbuzz.t .. 1/? 
#   Failed test '与えられた数字が3の時、Fizzを返すこと'
#   at t/01.fizzbuzz.t line 8.
#          got: '3'
#     expected: 'Fizz'
# Looks like you failed 1 test of 3.
t/01.fizzbuzz.t .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/3 subtests 
t/pod.t .......... ok   

Test Summary Report
-------------------
t/01.fizzbuzz.t (Wstat: 256 Tests: 3 Failed: 1)
  Failed test:  3
  Non-zero exit status: 1
Files=3, Tests=5,  0 wallclock secs ( 0.03 usr  0.02 sys +  0.10 cusr  0.02 csys =  0.17 CPU)
Result: FAIL
Failed 1/3 test programs. 1/5 subtests failed.

テスト通るようにコード直す

Fizz」を返すコードを追加。

sub say {
    my $self = shift;
    my $n    = shift||0;
    return "Fizz" if $n == 3;
    $n;
}

テスト通ることを確認

% perl Build ;perl Build test;                                        
Building FizzBuzz
t/00.load.t ...... 1/1 # Testing FizzBuzz 0.0.1
t/00.load.t ...... ok   
t/01.fizzbuzz.t .. ok   
t/pod.t .......... ok   
All tests successful.
Files=3, Tests=5,  1 wallclock secs ( 0.03 usr  0.02 sys +  0.10 cusr  0.02 csys =  0.17 CPU)
Result: PASS

「9」を与えた時のテストを追加

"与えられた数字が9の時、Fizzを返すこと"
t/01.fizzbuzz.t

is($c->say(9),"Fizz","与えられた数字が9の時、Fizzを返すこと");

テストが通らないことを確認

% perl Build ;perl Build test;                                       
Building FizzBuzz
t/00.load.t ...... 1/1 # Testing FizzBuzz 0.0.1
t/00.load.t ...... ok   
t/01.fizzbuzz.t .. 1/? 
#   Failed test '与えられた数字が9の時、Fizzを返すこと'
#   at t/01.fizzbuzz.t line 9.
#          got: '9'
#     expected: 'Fizz'
# Looks like you failed 1 test of 4.
t/01.fizzbuzz.t .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/4 subtests 
t/pod.t .......... ok   

Test Summary Report
-------------------
t/01.fizzbuzz.t (Wstat: 256 Tests: 4 Failed: 1)
  Failed test:  4
  Non-zero exit status: 1
Files=3, Tests=6,  1 wallclock secs ( 0.03 usr  0.02 sys +  0.10 cusr  0.02 csys =  0.17 CPU)
Result: FAIL
Failed 1/3 test programs. 1/6 subtests failed.

テスト通るようにコード直す

Fizz」判定を$n%3に変更。

sub say {
    my $self = shift;
    my $n    = shift||0;
    return "Fizz" if $n%3 == 0;
    $n;
}

テスト通ることを確認

% perl Build ;perl Build test;                                        
Building FizzBuzz
t/00.load.t ...... 1/1 # Testing FizzBuzz 0.0.1
t/00.load.t ...... ok   
t/01.fizzbuzz.t .. ok   
t/pod.t .......... ok   
All tests successful.
Files=3, Tests=6,  0 wallclock secs ( 0.03 usr  0.02 sys +  0.10 cusr  0.02 csys =  0.17 CPU)
Result: PASS

この辺りでコードをリファクタリングするらしい

以下ここまでの全コード。
判定部分を別サブルーチンに外出し。
lib/FizzBuzz.pm

package FizzBuzz;

use warnings;
use strict;
use Carp;
use version; our $VERSION = qv('0.0.1');

# Module implementation here
sub new { bless {} };
sub say {
    my $self = shift;
    my $n    = shift||0;
    return "Fizz" if _fizz($n);
    $n;
}

sub _fizz {
    $_[0]%3 == 0;
}

1;
__END__

「5」を与えた時のテストを追加

"与えられた数字が5の時、Buzzを返すこと"
t/01.fizzbuzz.t

is($c->say(5),"Buzz","与えられた数字が5の時、Buzzを返すこと");

テストが通らないことを確認

% perl Build ;perl Build test;                                        
Building FizzBuzz
t/00.load.t ...... 1/1 # Testing FizzBuzz 0.0.1
t/00.load.t ...... ok   
t/01.fizzbuzz.t .. 1/? 
#   Failed test '与えられた数字が5の時、Buzzを返すこと'
#   at t/01.fizzbuzz.t line 14.
#          got: '5'
#     expected: 'Buzz'
# Looks like you failed 1 test of 5.
t/01.fizzbuzz.t .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/5 subtests 
t/pod.t .......... ok   

Test Summary Report
-------------------
t/01.fizzbuzz.t (Wstat: 256 Tests: 5 Failed: 1)
  Failed test:  5
  Non-zero exit status: 1
Files=3, Tests=7,  0 wallclock secs ( 0.03 usr  0.02 sys +  0.10 cusr  0.02 csys =  0.17 CPU)
Result: FAIL
Failed 1/3 test programs. 1/7 subtests failed.

テスト通るようにコード直す

「Buzz」判定を追加。

sub say {
    my $self = shift;
    my $n    = shift||0;
    return "Fizz" if _fizz($n);
    return "Buzz" if _buzz($n);
    $n;
}
sub _buzz {
    $_[0]%5 == 0;
}
(以下省略)

テスト通ることを確認

% perl Build ;perl Build test;                                        
Building FizzBuzz
t/00.load.t ...... 1/1 # Testing FizzBuzz 0.0.1
t/00.load.t ...... ok   
t/01.fizzbuzz.t .. ok   
t/pod.t .......... ok   
All tests successful.
Files=3, Tests=7,  0 wallclock secs ( 0.03 usr  0.02 sys +  0.10 cusr  0.02 csys =  0.17 CPU)
Result: PASS

「15」を与えた時のテストを追加

"与えられた数字が15の時、FizzBuzzを返すこと"
t/01.fizzbuzz.t

is($c->say(15),"FizzBuzz","与えられた数字が15の時、FizzBuzzを返すこと");

テストが通らないことを確認

% perl Build ;perl Build test;                                        
Building FizzBuzz
t/00.load.t ...... 1/1 # Testing FizzBuzz 0.0.1
t/00.load.t ...... ok   
t/01.fizzbuzz.t .. 1/? 
#   Failed test '与えられた数字が15の時、FizzBuzzを返すこと'
#   at t/01.fizzbuzz.t line 17.
#          got: 'Buzz'
#     expected: 'FizzBuzz'
# Looks like you failed 1 test of 6.
t/01.fizzbuzz.t .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/6 subtests 
t/pod.t .......... ok   

Test Summary Report
-------------------
t/01.fizzbuzz.t (Wstat: 256 Tests: 6 Failed: 1)
  Failed test:  6
  Non-zero exit status: 1
Files=3, Tests=8,  0 wallclock secs ( 0.03 usr  0.02 sys +  0.10 cusr  0.02 csys =  0.17 CPU)
Result: FAIL
Failed 1/3 test programs. 1/8 subtests failed.

テスト通るようにコード直す

FizzBuzz」判定を追加。

sub say {
    my $self = shift;
    my $n    = shift||0;
    return "FizzBuzz" if _fizzbuzz($n);
    return "Fizz" if _fizz($n);
    return "Buzz" if _buzz($n);
    $n;
}

sub _fizzbuzz {
    $_[0]%15 == 0;
}
(以下省略)

テスト通ることを確認

% perl Build test                                                     
t/00.load.t ...... 1/1 # Testing FizzBuzz 0.0.1
t/00.load.t ...... ok   
t/01.fizzbuzz.t .. ok   
t/pod.t .......... ok   
All tests successful.
Files=3, Tests=8,  0 wallclock secs ( 0.03 usr  0.02 sys +  0.10 cusr  0.02 csys =  0.17 CPU)
Result: PASS

実行するスクリプト

run.pl

use warnings;
use strict;
use FizzBuzz;
use Perl6::Say;

my $c = FizzBuzz->new;
foreach (1..100){
    say $c->say($_);
}

感想

慣れてないのでいちいちテスト失敗させるのが面倒に感じた。
でも安心感が違いますね!
実践でも使っていきたいのでもうちょっと練習したいです