第5章 完成品に対してテストコードを実装

今回はテストコードを実装してみますが、ここでいうテストコードとはどういうものなのか、説明を始めると長くなりますのでまずは手を動かしながら実際にテストコードを実行できる状態にしてから最後にテストコードとはなにかについて考えるようにしてみます。

5.1 テストコードファイルの設置

Perl の場合、テストコードのファイルは慣例的に `t/` ディレクトリのなかに `sample.t` のように `.t` 拡張子を付けて保存することになっています。Perl がインストールされている環境ならば `prove` というコマンドを実行することでテストコードを実行できる仕組みがあり、Mojo のテストコードにおいても prove コマンドで実行することができます。

実際のソースコードはこちらの github からみれます。

5.1.1 ファイルを用意して実行

% mkdir t
% touch t/bulletin.t

bulletin.t の中身は下記のようにしておきます。

use Mojo::Base -strict;
use Test::More;
use Mojo::File qw(curfile);
use Test::Mojo;
use Mojo::Util qw{dumper};

# web アプリの実行スクリプトを指定
my $script = curfile->dirname->sibling('bulletin.pl');

# ルーティングテスト
my $t = Test::Mojo->new($script);
$t->get_ok('/')->status_is(200);

done_testing();

今回は docker を利用してアプリを起動しているのでもう一つターミナルのウインドウを開いて下記のように docker 経由でテストコードを起動します。

(ファイルを個別に指定)
% docker-compose exec web carton exec -- prove t/bulletin.t
t/bulletin.t .. ok   
All tests successful.
Files=1, Tests=2,  2 wallclock secs ( 0.02 usr  0.02 sys +  0.37 cusr  0.16 csys =  0.57 CPU)
Result: PASS

(指定しない場合は t/ の中のテストコードを全て実行)
% docker-compose exec web carton exec -- prove
t/bulletin.t .. ok   
...

(-v をすると詳細にテスト結果を出力)
% docker-compose exec web carton exec -- prove -v
...

ok 1 - GET /
ok 2 - 200 OK
1..2
ok
All tests successful.
Files=1, Tests=2,  3 wallclock secs ( 0.04 usr  0.03 sys +  0.49 cusr  0.21 csys =  0.77 CPU)
Result: PASS

5.1.2 実行コマンドを整理

docker を起動していない状態でテストコードだけをdockerで起動したい場合もあるとおもいます。そのような時のためにコマンド起動の仕組みを少し改善してみます。

(docker を起動中の場合は一旦停止してから、環境変数用のファイルを用意する
% mkdir etc
% touch etc/.env.test
(docker compose コマンド実行時に実行するファイルを用意)
% mkdir bin
% touch bin/compose-cmd-test.bash
% chmod +x bin/compose-cmd-test.bash
(今までつかっていたファイルを bin ディレクトリに移動)
mv compose-cmd.bash bin/

新しく用意したファイルの中身は下記のようにしてみてください。

etc/.env.test

BULLETIN_COMMNAD_FILE="./bin/compose-cmd-test.bash"

bin/compose-cmd-test.bash

#!/usr/bin/env bash
carton exec -- prove

docker-compose.yml を修正してみます

...
    command: '${BULLETIN_COMMNAD_FILE-./bin/compose-cmd.bash}'
...

下記のようなコマンドでdocker経由でテストコードだけを起動できるようになりました

% docker-compose --env-file ./etc/.env.test up

コマンドが増えたので README.md に Command という項目をつくって docker コマンドについてメモをのこすようにしました。詳細はリンクのgithubのソースコードを参照してみてください。

5.2 各機能に対してのテスト実装

これまでに実装してきた機能に対して実際にテストコードを作って実行してみます。

実際のソースコードはこちらの github からみれます。

5.2.1 ルーティングごとにテスト

これまでに4つのルーティングを実装しています、ルーティングごとに最低限のテストコードを作ってみます、下記を参考にしてみてください。

t/bulletin.t

# ルーティングごとにテスト
subtest '/' => sub {
    $t->get_ok('/')->status_is(200);
};

subtest '/list' => sub {
    # ... 同様
};

subtest '/create' => sub {
    # ... 同様
};

# 登録実行は空の値で登録するのでリダイレクト
subtest '/store' => sub {
    my $params = +{};
    $t->post_ok( '/store' => form => $params )->status_is(302);
};

このテストコードを実行すると警告がでる

(docker 経由でテストしてます)
% docker-compose --env-file ./etc/.env.test up             
Starting ctr-beginning-mojo ... done
Attaching to ctr-beginning-mojo
(undefで値が入力された場合トリムができないため警告がでる)
ctr-beginning-mojo | Use of uninitialized value $str in substitution (s///) at /usr/src/app/local/lib/perl5/Mojo/Util.pm line 350.
ctr-beginning-mojo | Use of uninitialized value $str in substitution (s///) at /usr/src/app/local/lib/perl5/Mojo/Util.pm line 351.
ctr-beginning-mojo | t/bulletin.t .. ok
ctr-beginning-mojo | All tests successful.
ctr-beginning-mojo | Files=1, Tests=6,  3 wallclock secs ( 0.03 usr  0.01 sys +  0.39 cusr  0.18 csys =  0.61 CPU)
ctr-beginning-mojo | Result: PASS
ctr-beginning-mojo exited with code 0
(テスト自体は通過するが好ましくない結果)

より安全な実装に修正する

bulletin.pl

post '/store' => sub {
    # ...

    # 値が空の場合は登録しない
    my $comment = $params->{comment};
    my $trimmed = '';
    if ($comment) {
        $trimmed = trim($comment);
    }
    # ...

実装を修正後テストコードを実行すると警告がでないことがわかる

% docker-compose --env-file ./etc/.env.test up
Starting ctr-beginning-mojo ... done
Attaching to ctr-beginning-mojo
ctr-beginning-mojo | t/bulletin.t .. ok
ctr-beginning-mojo | All tests successful.
ctr-beginning-mojo | Files=1, Tests=6,  3 wallclock secs ( 0.02 usr  0.02 sys +  0.34 cusr  0.20 csys =  0.58 CPU)
ctr-beginning-mojo | Result: PASS
ctr-beginning-mojo exited with code 0

5.2.2 一連の画面遷移をテスト

こんどは少し複雑なテストコードを作成します。トップページから一覧、登録、実行までを連続で実行します。データーベースへの値の登録もおこなうので、テストコード実行用のデーターベースの用意が必要になります。

公開用のデータベースとテスト用のデーターベースの切り替えは環境変数 MOJO_MODE を活用します。

t/bulletin.t

# ... 省略
# テストの時は強制的にモードを変更
$ENV{MOJO_MODE} = 'testing';

# テスト書き込み用のデータベースを用意する
init_db();

# ... 省略

# テスト用データベース準備
sub init_db {
    my $db     = curfile->dirname->sibling( 'db', 'bulletin.testing.db' );
    my $schema = curfile->dirname->sibling( 'db', 'bulletin.sql' );

    # system コマンドは失敗すると true
    my $cmd = "sqlite3 $db < $schema";
    system $cmd and die "Couldn'n run: $cmd ($!)";
    return;
}

# ... 省略
# トップページから登録、一覧までの挙動
subtest 'top -> list -> create -> store -> list' => sub {
    $t->get_ok('/')->status_is(200);

    # ... 省略

    # リダイレクト成功後の画面
    my $location_url = $t->tx->res->headers->location;
    $t->get_ok($location_url)->status_is(200);
    $t->content_like(qr{\Q$val\E});
};

データベースとの接続部分を修正

bulletin.pl

sub teng {
    my $dsn_str = 'dbi:SQLite:./db/bulletin.db';
    if ( $ENV{MOJO_MODE} && $ENV{MOJO_MODE} eq 'testing' ) {
        $dsn_str = 'dbi:SQLite:./db/bulletin.testing.db';
    }
    # ... 省略
    return $teng;
}

最終的に重複していた '$t->get_ok('/')->status_is(200);' 部分を修正してテストコードを実行すると下記のような結果になる

% docker-compose --env-file ./etc/.env.test up
Starting ctr-beginning-mojo ... done
Attaching to ctr-beginning-mojo
ctr-beginning-mojo | t/bulletin.t .. ok
ctr-beginning-mojo | All tests successful.
(subtest の数 5 が Tests の数になっていることがわかる)
ctr-beginning-mojo | Files=1, Tests=5,  3 wallclock secs ( 0.03 usr  0.02 sys +  0.43 cusr  0.18 csys =  0.66 CPU)
ctr-beginning-mojo | Result: PASS
ctr-beginning-mojo exited with code 0

5.3 どこまでテストをするのか

ここまでテストコードの実装をすすめてきていろいろと疑問に思うことがあったと思います。テストとはなんの目的でやるものなのか、テストコードはどこまで実装するのがよいのか、Perl の中級者以上の方向けの書籍になりますが、下記「モダンPerl入門」という書籍にこの辺が詳しく書かれていますので興味がある方は参考にされると良いと思います。

下記に私が思うテストコードとはこういうものだということを箇条書きにまとめておきましたので参考にしてみてください。
  • テストコード
    • メリット
      • 手動で行う動作確認を機械的に自動的に行うことでテストの実行間違いを防ぐ効果が期待できる
      • 手動よりはやく動作確認を行うことができる
      • 機能を追加、変更によって、既存のコードの挙動に変化が起こった時にはやく気づける
    • デメリット
      • テストコード自体を書く時間が必要になる
      • テストコードの書き方を習得するのに時間がかかる
      • どこまで詳細にテストを書くかの加減が難しい
      • テストコード自体の保守の問題が発生する
    • 実装するタイミングについて
      • 実装コードを書いてからテストコードを書く
        • 実装コードの時間とテストコードの時間の両方の時間がかかる
        • 実装コードの挙動を正として挙動にあわせてテストを書く
        • あきらかに実装コードがおかしい場合は実装コードを修正する
        • テストコードで再現するのがとても難しい場合はテストコードを断念する
        • テストコードを断念する見極めが難しい
      • テストコードを書いてから実装コードを書く
        • 実装コードの時間とテストコードの時間の両方の時間がかかる
        • テストコードを書く段階でロジックは判明するので実装コードの時間は短くなる
        • テストコードの挙動を正として挙動にあわせて実装コードを書く
        • あきらかにテストコードがおかしい場合はテストコードを修正する
        • 実装より先にテストコードを書くというのはとても難しい
        • テストコードを断念する見極めが難しい
        • 実装コードで再現するのがとても難しい場合というのはほとんど存在しない
      • 実装コードとテストコードを同時進行で書いてゆく
        • 結果的に実装コードだけ書く時間より短縮できることが多い
        • テストコードを断念する見極めが早い段階でわかる
        • 実装より先にテストコードを書くよりも難易度が下がる
      • テストコードの項目について
        • 大まかに成功系と失敗系にわかれる
        • 一般的には成功系のテストは少しにして失敗系のパターンを沢山書くほうがいいとされる
        • 失敗系のパターンをどこまで想定するかの見極めがとても難しい、やり過ぎに注意
      • テスト項目の見極め
        • 沢山の項目を書いたほうが良いと思われがちだがテストコードの保守問題を忘れてはいけない
        • webアプリについては仕様自体が頻繁に変更されるので項目そのものが変更になることがある

5.4 まとめ

5.5 次回

今回はすでに実装されているコードに対してのテストコードの実装をしてみました。テストコードをたくさん書いたほうが実装コードの品質は上がってゆきますが、テストコードをたくさん書いたからといって機能が増えるというわけではないので、その辺のバランス感覚というのは大事なんだと思います。

次回はテストコードと実装を同時に進めるやり方を紹介したいと思います。