テスト駆動開発のすすめ

hachiojipmに行ってきたのですが#4でも#5でもTestを書くのが難しいという声が聞こえたので「テストは書いてみると簡単」「テストがあると開発が楽」という事を伝えてみようと努力する試みです。

ということでサンプルコードを書いてみました。

https://github.com/okamuuu/Sample-Plack-Test

紹介するサンプルコードについて

ここで紹介しているスクリプトはある男がBlogを作ろうと思ったがどうせたいしたことしないので俺俺WaFをつくってやろうとして実際にやったテスト駆動開発です。

おもむろにt/web.tとかつくってみる

最初にテストを書いてみましょう。

#!/usr/bin/env perl
use strict;
use warnings;
use Test::Most;
use Plack::Test;
use HTTP::Request::Common qw/GET POST/;

my $app = sub {
    return [
        200, [ "Content-Type" => "text/plain", "Content-Length" => 31 ],
        ["hello, I'm okamuuu. Thank you!!"]    ];  
};

test_psgi $app, sub {
    my $cb  = shift;
    my $res = $cb->( GET "/" );

    is $res->code, 200;
    is $res->header('Content-Type'), 'text/plain';
    is $res->content, "hello, I'm okamuuu. Thank you!!";
};

done_testing();

テスト実行。もちろん成功

% prove -vl t/web.t
t/web.t .. 
ok 1
ok 2
ok 3
1..3
ok
All tests successful.

t/web.tを書き換える

では実際にcode-refを返すシンプルな俺々WaFをつくってみましょう。
以下のようにちょっと書き換えます。

#!/usr/bin/env perl
use strict;
use warnings;
use Test::Most;
use Plack::Test;
use HTTP::Request::Common qw/GET POST/;

use_ok('MyApp::Web::Handler');
my $app = MyApp::Web::Handler->app();

test_psgi $app, sub {
    my $cb  = shift;
    my $res = $cb->( GET "/" );

    is $res->code, 200;
    is $res->header('Content-Type'), 'text/plain';
    is $res->content, "hello, I'm okamuuu. Thank you!!";
};

done_testing();

テスト実行。もちろん失敗

:!prove -vl t/web.t
t/web.t ..
not ok 1 - use MyApp::Web::Handler;
<省略>

lib/MyApp/Web/Handler.pmを追加

まずはuse_okのテストを通してみましょう。以下のようにuseできるだけのパッケージをつくります。

package MyApp::Web::Handler;
use strict;
use warnings;

1;

テスト実行。use_okはできているのですが、done_testingに到達するまえにプログラムがdieしています。まあそんなメソッドないですからねえ。

:!prove -vl t/web.t
t/web.t .. 
ok 1 - use MyApp::Web::Handler;
Can't locate object method "app" via package "MyApp::Web::Handler" at t/web.t line 21.
# Tests were run but no plan was declared and done_testing() was not seen.

<省略>

lib/MyApp/Web/Handler.pmにメソッドを追加

code-refを返すメソッドを追加してあげます。

package MyApp::Web::Handler;
use strict;
use warnings;

sub app {
    return sub {
        [   
            200,
            [ "Content-Type" => "text/plain", "Content-Length" => 31 ],
            ["hello, I'm okamuuu. Thank you!!"]
        ];
      };
}

1;

テスト実行。成功します。こんなテストでも通ると気分がよくなります。

:!prove -vl t/web.t
t/web.t ..
ok 1 - use MyApp::Web::Handler;
ok 2
ok 3
ok 4
1..4
ok
All tests successful.

テストがあれば安心して改修できます。

MyApp::Web::Handler->appが返すcode-refはPlackからハッシュリファレンスを受け取っています。
このハッシュリファレンスからユーザーが何を要求しているのか読み解くことになるます。

なのですがMyApp::Web::Handlerはかまわずに配列リファレンスを返しています。ちょっと改造します。

package MyApp::Web::Handler;
use strict;
use warnings;
use Plack::Request;

sub app {
    return sub {
        my $env = shift;

        my $request  = Plack::Request->new($env);
        my $response = $request->new_response(200);

        $response->content_type('text/plain');
        $response->body("hello, I'm okamuuu. Thank you!!");
      
        return $response->finalize();
      };  
}

1;

テスト実行。成功しました。テストがあればコードを書き直した後のチェックが早くて便利ですね。

:!prove -vl t/web.t                                                 
t/web.t .. 
ok 1 - use MyApp::Web::Handler;
ok 2
ok 3
ok 4
1..4
ok
All tests successful.

「遊ぼう」と言えば 「遊ぼう」と答え 「好き」と言えば 「好き」と答える。

いまのままだと「遊ぼう」と言っても「好き」と言っても「ばか」って言っても全部「おっす。おれokamuuu。ありがとよ!!」としか答えません。

とりあえず「またね」と言われたら「またね!!」って返事するようにしましょう。テストを書き換えます。

#!/usr/bin/env perl
use strict;
use warnings;
use Test::Most;
use Plack::Test;
use HTTP::Request::Common qw/GET POST/;

use_ok('MyApp::Web::Handler');
my $app = MyApp::Web::Handler->app();

test_psgi $app, sub {
    my $cb  = shift;
    my $res = $cb->( GET "/" );

    is $res->code, 200;
    is $res->header('Content-Type'), 'text/plain';
    is $res->content, "hello, I'm okamuuu. Thank you!!";
};

test_psgi $app, sub {
    my $cb  = shift;
    my $res = $cb->( GET "/bye" );

    is $res->code, 200;    is $res->header('Content-Type'), 'text/plain';
    is $res->content, "bye bye!!";
};

done_testing();

テストを実行するとやっぱり失敗してしまいました。

:!prove -vl t/web.t
t/web.t ..
ok 1 - use MyApp::Web::Handler;
ok 2
ok 3
ok 4
ok 5
ok 6
not ok 7

#   Failed test at t/web.t line 27.
#          got: 'hello, I'm okamuuu. Thank you!!'
#     expected: 'bye bye!!'
1..7
# Looks like you failed 1 test of 7.

ふりわける

ユーザーのリクエストに応じてどのメソッドを呼べばいいのか判定する処理を書けば良い気がします。なんかそんなのをつくります*1;

MyApp::Web::Handlerを作成します。

package MyApp::Web::Handler;
use strict;
use warnings;
use MyApp::Web::Router::Sinatraish;
use Plack::Request;

sub app {
    return sub {
        my $env = shift;

        my $router = MyApp::Web::Router::Sinatraish->router;
        my $route  = $router->match($env) or return [ 404, [], ['not found'] ];

        my $request = Plack::Request->new($env);
        my $response = $route->{code}->($request);

        return $response->finalize();
      };
}

1;

それからMyApp::Web::Router::Sinatraishを作成します。

package MyApp::Web::Router::Sinatraish;
use strict;
use warnings;
use Router::Simple::Sinatraish;

get '/' => sub { 
    my $request = shift;
    my $response = $request->new_response(200);

    $response->content_type('text/plain');
    $response->body("hello, I'm okamuuu. Thank you!!");

    return $response;
};

get '/bye' => sub {
    my $request = shift;
    my $response = $request->new_response(200);

    $response->content_type('text/plain');
    $response->body("bye bye!!");

    return $response;
};

1;

テストすると成功します。

:!prove -vl t/web.t
t/web.t .. 
ok 1 - use MyApp::Web::Handler;
ok 2
ok 3
ok 4
ok 5
ok 6
ok 7
1..7
ok
All tests successful.

まとめ

そんなこんなで少しずつWaFっぽくなってきたと思います。このようにテストを少しずつ変化させながら開発したりすると実装に集中することができるのですごく楽です。
ちなみに私はviからproveコマンドを呼んでいます*2

そんなこんなでテストを書くのが難しい、という声を聞きますが、私の場合は逆にこうやって開発したほうが手間がかからないので楽だと思います。

だからみんなもテストを書いてみませんか?

*1:文章が乱れてきました。疲れてきたからです。

*2:「,t」でショートカットを用意しています。Perl Hackに書いてあるやつです