BPStudy #29 TDD

はじめに

tokuhiromさんとペアを組ませていただいたのですが、本当に何もできず(そもそも緊張しすぎてしゃべることすらできず)本当に嫌な思いをさせてしまって申し訳ありませんでした。
そんなごめんなさいエントリです。

お題

LRUCacheをつくる

Limitを決めておいて、そのLimitの数だけ値をsetする
getをすることもできて、getすると使われる(残る)
値をsetしたときにLimitより多くなった場合、古いものから消える

TDDのサイクル

TDDのサイクルは、「動くものを書いてきれいにしていく」というプロセスだそうです。

1. テストを書き
2. そのテストを実行して失敗させ(Red)
3. 目的のコードを書き
4. テストを成功させ(Green)
5. テストが通るままでリファクタリングを行う(Refactoring)
6. 1〜5を繰り返す

Skeletonをつくります

今回はModule::Starterを使いました。pmsetupじゃなくてごめんなさい

$ module-starter --module=LRUCache
$ perl Makefile.PL
$ make test

ということでまずテストを書こう

まずまずテストを書いてみます。自信がまったくないのでgetとsetだけ><

$ vi t/01-main.t
#!/usr/bin/perl

use strict;
use warnings;

use Test::More;
use LRUCache;

my $lru = LRUCache->new(); 
$lru->set('a' => 'TestA');
$lru->set('b' => 'TestB');
$lru->set('c' => 'TestC');
$lru->get('a');

done_testing;


実行してみます。こけます。

$ prove -lvr t/01-main.t 
t/01-main.t .. Can't locate object method "new" via package "LRUCache" at t/01-main.t line 9.
Dubious, test returned 255 (wstat 65280, 0xff00)
No subtests run 

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

テストに合うように実装してみる

まずは今の状態でテストが通るようにやってみます

package LRUCache;

use warnings;
use strict;

use Moose;

has 'data' => (
    is => 'rw',
    isa => 'HashRef',
    default => sub { +{} },
);

sub set {
    my ($self, $key, $value) = @_; 
    $self->data->{$key} = $value;
}

sub get {
    my ($self, $key) = @_; 
    return $self->data->{$key};
}

1;

いっかい通してみます。

$ prove -lvr t/01-main.t
t/01-main.t ..
ok 1
1..1
ok
All tests successful.
Files=1, Tests=1,  0 wallclock secs ( 0.03 usr  0.01 sys +  0.23 cusr  0.02 csys =  0.29 CPU)
Result: PASS

いったんgetとsetはできました。

LRUになるように変更してみる

まずはテストから。

my $lru = LRUCache->new(limit => 2); 
$lru->set('a' => 'TestA');
$lru->set('b' => 'TestB');
$lru->set('c' => 'TestC');
is $lru->get('a'), undef; # 2こしかないから、cを入れたときにaが消えてほしい

テストを書き始めて、もっと1こずつテストしたほうがいいと気づきました。
いっぱい書いてみます。

#!/usr/bin/perl

use strict;
use warnings;

use Test::More;
use LRUCache;

subtest 'test1' => sub {
    my $lru = LRUCache->new(limit => 2); 
    $lru->set('a' => 'TestA');
    $lru->set('b' => 'TestB');
    is $lru->get('a'), 'TestA';
    is $lru->get('b'), 'TestB';
    done_testing;
};

subtest 'test2' => sub {
    my $lru = LRUCache->new(limit => 2); 
    $lru->set('a' => 'TestA');
    $lru->set('b' => 'TestB');
    $lru->set('c' => 'TestC');
    is $lru->get('a'), undef;
    is $lru->get('b'), 'TestB';
    is $lru->get('c'), 'TestC';
    done_testing;
};

subtest 'test3' => sub {
    my $lru = LRUCache->new(limit => 2); 
    $lru->set('a' => 'TestA');
    $lru->set('b' => 'TestB');
    is $lru->get('c'), undef;
    is $lru->get('a'), 'TestA';
    done_testing;
};

done_testing;

subtestというのは今回はじめて知りました。Test::Moreでできるのですね。
ここでもtokuhiromさんに助けていただくなんて><

http://d.hatena.ne.jp/tokuhirom/20100118/1263800343


ほいで、実装してみました。
教えてもらったのはもっとかっこよかったのですが、申し訳ないです覚えきれなかったのでだいぶひどい感じになっています><

package LRUCache;

use warnings;
use strict;

use Moose;
use Data::Dumper;

has 'limit' => (
    is => 'ro',
    isa => 'Int',
    default => 1,
);

has 'data' => (
    is => 'rw',
    isa => 'HashRef',
    default => sub { +{} },
);

has 'used' => (
    is => 'rw',
    isa => 'HashRef',
    default => sub { +{} },
);

has 'counter' => (
    is => 'rw',
    isa => 'Int',
    default => 0,
);

sub get {
    my ($self, $key) = @_; 

    return undef unless defined $self->data->{$key};
    if ($self->used->{$key}) {
        $self->used->{$key} = $self->counter($self->counter + 1);
        return $self->data->{$key};
    }
}

sub set {
    my ($self, $key, $value) = @_;
    $self->data->{$key} = $value;
    $self->used->{$key} = $self->counter($self->counter + 1);

    if ($self->limit < scalar keys %{$self->used}) {
        my @store = sort { $self->used->{$b} <=> $self->used->{$a} } keys %{ $self->used };
        $#store = $self->limit - 1;
        my $new_hash;
        foreach my $k (@store) {
            $new_hash->{$k} = $self->used->{$k};
        }
        $self->used($new_hash);
    }
}

1;

setしたりgetしたりするたびにカウンタを増やして、
usedという中にいまキャッシュにはいっているべきkeyとカウンタが入るようにしました。
配列の要素数変更とかかなりごりっとしてて目もあてられない><でもうごくのでいったん。。


テストしてみます。

$ prove -lvr t/01-main.t
t/01-main.t .. 
    ok 1
    ok 2
    1..2
ok 1 - test1
    ok 1
    ok 2
    ok 3
    1..3
ok 2 - test2
    ok 1
    ok 2
    1..2
ok 3 - test3
1..3
ok
All tests successful.
Files=1, Tests=3,  0 wallclock secs ( 0.03 usr  0.01 sys +  0.23 cusr  0.02 csys =  0.29 CPU)
Result: PASS

とおりました。

limitが途中で変わっても大丈夫なように作る

まずまずテストから

subtest 'test4' => sub {
    my $lru = LRUCache->new(limit => 3);
    $lru->set('a' => 'TestA');
    $lru->set('b' => 'TestB');
    $lru->set('c' => 'TestC');
    $lru->limit(2);
    is $lru->get('a'), undef;
    is $lru->get('c'), 'TestC';
    done_testing;
};


実装を変更します。limitが変更したときにusedにあるものを変更したいので、
Mooseのtriggerを使います。
(変更部分のみ記します)

has 'limit' => (
    is => 'rw',
    isa => 'Int',
    default => 1,
    trigger => sub {
        my ($self, $new, $old) = @_;
        return unless defined $old;
        $self->used($self->_used_hash);
    },
);

sub set {
    my ($self, $key, $value) = @_;
    $self->data->{$key} = $value;
    $self->used->{$key} = $self->counter($self->counter + 1);

    if ($self->limit < scalar keys %{$self->used}) {
        my $new_hash = $self->_used_hash();
        $self->used($new_hash);
    }
}

sub _used_hash {
    my ($self) = @_;

    my @store = sort { $self->used->{$b} <=> $self->used->{$a} } keys %{ $self->used };
    $#store = $self->limit - 1;
    my $new_hash;
    foreach my $k (@store) {
        $new_hash->{$k} = $self->used->{$k};
    }
    return $new_hash;
}

最後にテストしてみます。

$ prove -lvr t/01-main.t
t/01-main.t .. 
    ok 1
    ok 2
    1..2
ok 1 - test1
    ok 1
    ok 2
    ok 3
    1..3
ok 2 - test2
    ok 1
    ok 2
    1..2
ok 3 - test3
    ok 1
    ok 2
    1..2
ok 4 - test4
1..4
ok
All tests successful.
Files=1, Tests=4,  1 wallclock secs ( 0.03 usr  0.01 sys +  0.24 cusr  0.02 csys =  0.30 CPU)
Result: PASS

とおりました><やった!


まとめ

もっと教えてもらったものをメモっておけばよかったとか
そもそも書けよとかありますが本当にすみませんすみませんすみません><
しかも教えてもらったことさえ満足にできているか・・・><。。

そしてもっときれいに書ける方法かんがえます。


YAPCの時にMooseを教えてもらったときにテストを埋めるように開発するスタイルを教えてもらっていたので
なんとか復習することができました。ありがとうございます。
超てんぱっている中優しく教えてくださって本当にありがとうございました。


そういえばTDDで教えてもらったことをメモってたのに全く書いてないですね。

「ふるまいベースのテスト」ということがとても印象的でした。
実装の細かいやりかたのテストはふつうにwarnとかで出して、
入力、出力はこうあるべきというところだけテストで書くんだというのがわかりました。
昨日ちょうど、テストに何を書いたらいいのかということを社内で話していたので今回わかってよかったです><


ねむむい明日合宿なのでもう帰ります@マクド