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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
テストに合うように実装してみる
まずは今の状態でテストが通るようにやってみます
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とかで出して、
入力、出力はこうあるべきというところだけテストで書くんだというのがわかりました。
昨日ちょうど、テストに何を書いたらいいのかということを社内で話していたので今回わかってよかったです><
ねむむい明日合宿なのでもう帰ります@マクド