1. あんなテスト・こんなテスト (This and That about testing)
土田 拓也(Takuya Tsuchida)
@tsucchi
2011/10/15
id: tsucchi1022
エレクトロニクス事業本部
Public/公開情報 -1-
2. Abstract
テストの話をします
(I'll be talking about testing)
とくに「テストしにくい部分をどのようにテストするか」について話し
ます
(Especially, I'll talk about how to test the part which is hard to test)
Public/公開情報 -2-
3. About Me
土田 拓也(Takuya Tsuchida)
所属: 凸版印刷株式会社
エレクトロニクス事業本部 システム開発部
(TOPPAN PRINTING Co., LTD
Electronics Division System Development team)
仕事: MES(製造実行システム)の開発・運用など
(Develop and operate MES(Manufacturing Execution System))
– DB 設計したり、SQL 書いたり、Perl 書いたりしています
(designing DB schema, writing SQL and Perl etc)
CPAN(PAUSE): TSUCCHI
id(hatena): tsucchi1022
twitter: @tsucchi
github: https://github.com/tsucchi
Public/公開情報 -3-
4. testcodes
テストコード、書いてますか?
(Do You Write testcodes?)
テストコードとは(What is the testcode?)
– 「入力」と「その入力に対して、期待する出力」を書いて、一致するかど
うかを検証するプログラム
(programs which validates 'input' and 'expected output from the
provided input' are correct.)
Public/公開情報 -4-
5. Automated Testing (2)
Example) testing add() subroutine
#!/usr/bin/perl -w
use strict;
use warnings;
use Test::More;
Subroutine to be tested
sub add {
my (@inputs) = @_;
my $result = 0;
for my $input ( @inputs ) {
$result += $input;
}
return $result;
}
Input for test
# testing 'add' subroutine Expected output
is( add(1, 2), 3 );
is( add( (1 .. 10) ), 55 );
done_testing();
Public/公開情報 -5-
6. Strong points and Weak Points
長所(strong points)
– 繰り返し実行できる(It enables to run any time)
• 改修やリファクタリングでエンバグしていないか容易
に調べられる(You can easily find whether enbug or not when you
finished bug-fix or refactorings)
• Jenkins などの CI サーバと組み合わせることで、コ
ミット時などの任意のタイミングでテストを実施でき
る(Combine with CI server, It is enable to run tests any time such as
after commit)
短所(weak points)
– イニシャルコストが上がる (increase initial costs)
– テストを書きにくい場合がある (Sometimes, It is hard to write testcodes)
Public/公開情報 -6-
7. When It is hard to write testcode
「入力」や「出力」が明確ではなかったり、作りにくい場合
(Inputs or outputs are ambiguous or hard to make these)
– 標準入出力(STDIN/STDOUT)
– コマンドラインオプション (commandline options)
– 時刻(system clock)
– DB
– etc.
これらを、「なんとかする」やり方を紹介します
(I'll introduce how to deal with such things)
Public/公開情報 -7-
8. 標準入出力(STDIN/STDOUT)
コマンドラインオプション (commandline options)
時刻(system clock)
DB
etc.
Public/公開情報 -8-
9. Tests for STDIN/STDOUT
Principle
– 内部のロジックが良くテストされているなら、無理して実施する必
要は無い(If internal logic is well tested, It is no need to test STDIN/STDOUT
forcefully)
Example Situation
– コマンドラインツールのテストをしたい(want to test command-line tool)
– 外部モジュールの中間出力が見たいが、その内容が標準出力
を使っている(middle output for external modules, but the output is printed in
STDOUT/STDERR)
– warn/carp の内容を確認したい(want to check output by warn/carp)
Solution
– use IO::Scalar
– use Capture::Tiny(for STDOUT/STDERR)
– use IO::Capture::STDOUT/STDERR
– tie STDIN/STDOUT/STDERR
Public/公開情報
10. Automated Testing (2)
Ex 1)using IO::Scalar and capture STDIN
#!/usr/bin/perl -w
use strict;
use warnings;
use Test::More;
sub add {
my $result = 0;
while( <STDIN> ) {
chomp;
$result += $_;
}
return $result;
}
subtest 'add', sub {
my $inputs = "1n2n3n"; # input from STDIN
open my $stdin_fh, '<', $inputs;
local *STDIN = *$stdin_fh; # replace default STDIN
is( add(), 6 );
};
done_testing();
Public/公開情報 - 10 -
11. Automated Testing (2)
Ex 2) using Capture::Tiny(for STDOUT/STDERR)
#!/usr/bin/perl -w
use strict;
use warnings;
use Test::More;
use Capture::Tiny qw(capture);
sub add {
my($a, $b) = @_;
print $a + $b;
}
my ($stdout) = capture {
add(1, 2);
};
is($stdout, 3);
done_testing();
– 簡単に使えるので、Test::Warn の代用とするのも良いと思う
(I think it's good idea to use this module alternate for Test::Warn)
Public/公開情報 - 11 -
12. 標準入出力(STDIN/STDOUT)
コマンドラインオプション (commandline options)
時刻(system clock)
DB
etc.
Public/公開情報 - 12 -
13. Tests for command-line args
Principle
– 内部のロジックが良くテストされているなら、無理して実施する必
要は無い(If internal logic is well tested, It is no need to test
STDIN/STDOUT forcefully)
Example Situation
– コマンドラインツールのテストをしたい(want to test command-line
tool)
– GetOpt::* を使わず、自前でオプション解析しているのを直したい
(want to fix because it has self-implemented command-line
args analysis)
Solution
– @ARGV を書き換える
Public/公開情報
14. Tests for command-line args
Ex) testing command-line args
#!/usr/bin/perl -w
use strict;
use warnings;
use Test::More;
our $a_str = undef;
# 本当は別 package にある / in real case, this subroutine is defined in other package
sub read_args {
while ( $_ = shift @ARGV ) {
if ( $_ =~ /^-a$/ ) {
$a_str = shift @ARGV;
}
# ... other option analyses are follows
}
}
subtest 'a option with arg test', sub {
$a_str = undef;
local @ARGV = ("-a", "a_value"); #ここにオプションを指定 / passes options here
read_args();
is($a_str, "a_value");
};
done_testing();
Public/公開情報 - 14 -
15. 標準入出力(STDIN/STDOUT)
コマンドラインオプション (commandline options)
時刻(system clock)
DB
etc.
Public/公開情報 - 15 -
16. Tests for system clock
Principle
– 時刻に依存せずテストが書かれるべき(Tests should be written in no
depencency to system clock)
Example Situation
– ログの日付フォーマットが正しいかチェックしたい(want to test
datetime format in logs)
– ロット番号など、日付によって処理内容が変わるものをテストした
い(want to test what changes depending on datetime such as lot-no)
Solution
– 時刻を改竄する(alter perl's system clock)
• use Test::MockTime
• use Time::Mock
• CORE::GLOBAL::time() を書き換える(override
CORE::GLOBAL::time)
Public/公開情報
17. Tests for system clock
Ex) Test::MockTime
#!/usr/bin/perl
use strict;
use warnings;
BEGIN { $ENV{TZ} = 'JST' }
use Test::MockTime qw(set_fixed_time);
use Test::More;
use POSIX qw(strftime);
sub some_lot_no { return strftime("%Y%m%d-%H%M%S", localtime()); }
set_fixed_time('2009-03-23T11:22:33');
is( some_lot_no(), '20090323-112233');
done_testing();
Public/公開情報 - 17 -
18. 標準入出力(STDIN/STDOUT)
コマンドラインオプション (commandline options)
時刻(system clock)
DB
etc.
Public/公開情報 - 18 -
19. Tests for DB
Principle
– 基本はモック(DBD::Mock)を使うべき(Mock should be used)
– ビジネスロジックと DB は切り離すべき(Business logics and DB should be
separated)
Example Situation
– ストアドプロシージャをテストしたい(want to test stored procedure)
– ORM を使わず、生の DBI を使っているので SQL をテストしたい
(want to test SQL because we don't use ORM)
Solution
– データを流し込む(load data into DB)
– Test::mysqld + something
• Test::Fixture::DBI
• Test::DBUnit
• Test::DataLoader::MySQL
Public/公開情報
20. 標準入出力(STDIN/STDOUT)
コマンドラインオプション (commandline options)
時刻(system clock)
DB
etc.
Public/公開情報 - 20 -
21. exit measures(1)
テスト中に exit が呼ばれると、意図せずテストが通ってしまう
(If exit() is called, tests are passed accidentally)
Example Situation
– 他人のコードを引き継いだ際(when takeover someone's code)
– ライブラリがエラー処理後に exit を呼んでいた(library routine calls
exit after error handling)
Solution
– exit()の上書き(override exit)
– exit を使っている関数/メソッドの上書き(override
subroutine/method which uses exit)
Public/公開情報 - 21 -
22. Exit measures(2)
Ex1) exit() causes problem
#!/usr/bin/perl -w
use strict;
use warnings;
use Test::More 'no_plan';
my $important_value = '';
sub evil_operation { $important_value = "aaa"; exit 0; }
ok(1);
evil_operation();
is( $important_value, '' );
% prove exit.t
exit.t .. ok
All tests successful.
Files=1, Tests=1, 0 wallclock secs ( 0.04 usr 0.00 sys +
0.01 cusr 0.01 csys = 0.06 CPU)
Result: PASS
– This test successes unexpectedly
Public/公開情報 - 22 -
23. Exit measures(3)
Ex2) measured exit() call
#!/usr/bin/perl -w
use strict;
use warnings;
use Test::More 'no_plan';
BEGIN { *CORE::GLOBAL::exit = sub { die 'unexpected exit called!' } } # ADD THIS!
my $important_value = '';
sub evil_operation { $important_value = "aaa"; exit 0; }
ok(1);
evil_operation();
is( $important_value, '' );
% prove exit_measured.t
exit_measured.t .. 1/? unexpected exit called! at
exit_measured.t line 6.
...(snip)
Result: FAIL
– It's OK. It should be failed.
Public/公開情報 - 23 -
24. setUp/tearDown(1)
xUnit を使っていた人は setUp/tearDown が使いたいかも
(xUnit users may want to use setup/tearDown)
– それ Test::Class で出来るよ!
(Test::Class enables it!)
#!/usr/bin/perl -w
use strict;
use warnings;
use Test::Class;
MyTest->runtests();
package MyTest;
use parent qw(Test::Class);
use Test::More;
sub set_up :Test(setup) { diag("setup"); }
sub tear_down :Test(teardown) { diag("teardown"); }
sub my_test :Test(1) { ok(1); #this is some test }
sub my_test2 :Test(1) { is("1", "1"); #this is another test }
Public/公開情報 - 24 -
25. setUp/tearDown(2)
でも書き方が変わるのは面倒くさい
(But it isn't good that how to write test is changed)
– subtest + Hook::LexWrap
#!/usr/bin/perl -w
use strict;
use warnings;
use Test::More;
use Hook::LexWrap;
wrap 'subtest',
pre => sub { diag("setup"); }, #alternate for setup
post => sub { diag("teardown");} #alternate for teardown
;
subtest 'my_test', sub { ok(1); };
subtest 'my_test2', sub { is("1", "1"); };
done_testing();
Public/公開情報 - 25 -
26. setUp/tearDown(3)
実行例(execution example)
% perl setup_teardown.t
# setup setup called
ok 1
1..1
ok 1 - my_test test called
# teardown
# setup teardown called
ok 1
1..1
ok 2 - my_test2
# teardown
1..2
Public/公開情報 - 26 -
27. Caution(1)
今回紹介したテクニックをプロダクションコード側で使わない
(Don't use these techniques in production code)
– プロダクションコードでモンキーパッチしたり、時間や
CORE::GLOBAL::* を書き換えたり、@ARGV 書き換えた
り、標準入出力捕まえたりしないこと
(In production code, don't monkey-patch, don't alter system clock, don't replace
CORE::GLOBAL::*, don't replace @ARGV, and don't capture STDIN/STDOUT)
– テストでは有効なテクニックでも、プロダクションコードで使
うと妙なバグに振り回されるかもしれません
(These techniques are useful in testcode, but you may encounter curious bugs if
using it in production code)
Public/公開情報 - 27 -
28. Caution(2)
「テストしにくい部分を何とかしたい!」という考えは基本的には何
かが間違っています
(I think it is wrong opinion such as 'I want to manage testcode which is hard to be written')
– 段階的に直していくべき(It should be fixed gradually)
– Mock 使うとか、他の部分をテストしてカバーするとか
(using Mock or tests other parts to cover it)
Public/公開情報 - 28 -
29. Conclusion
テストしにくいものも、Perl だと結構なんとかなります
(Sometimes it is hard to write testcode, but Perl provides power to make it possible)
「どうやったら、このテストしにくいコードを何とかできるか」を考え
るのは結構楽しい
(It is fun thinking about how to write testcode which is hard to be written)
– とくにレガシーコードを相手にする場合は
(especially for legacy codes)
テストを書きましょう!辛いテストでも Perl なら何とかな
ります!
(Let's write testcode! Perl enables you to provide power to write hard tests)
Public/公開情報 - 29 -