あんなテスト、こんなテスト(this and that about testing)

31
- 1 - Public/公開情報 エレクトロニクス事業本部 2011/10/15 土田 拓也(Takuya Tsuchida) @tsucchi id: tsucchi1022 あんなテスト・こんなテスト (This and That about testing)

Transcript of あんなテスト、こんなテスト(this and that about testing)

Page 1: あんなテスト、こんなテスト(this and that about testing)

- 1 -Public/公開情報

エレクトロニクス事業本部

2011/10/15

土田 拓也(Takuya Tsuchida)@tsucchi

id: tsucchi1022

あんなテスト・こんなテスト (This and That about testing)

Page 2: あんなテスト、こんなテスト(this and that about testing)

- 2 -Public/公開情報

Abstract

テストの話をします

(I'll be talking about testing)

とくに「テストしにくい部分をどのようにテストするか」について話します

(Especially, I'll talk about how to test the part which is hard to test)

Page 3: あんなテスト、こんなテスト(this and that about testing)

- 3 -Public/公開情報

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

Page 4: あんなテスト、こんなテスト(this and that about testing)

- 4 -Public/公開情報

testcodes

テストコード、書いてますか?

(Do You Write testcodes?)

テストコードとは(What is the testcode?)

– 「入力」と「その入力に対して、期待する出力」を書いて、一致するかどうかを検証するプログラム

(programs which validates 'input' and 'expected output from the provided input' are correct.)

Page 5: あんなテスト、こんなテスト(this and that about testing)

- 5 -Public/公開情報

Automated Testing (2)

Example) testing add() subroutine

#!/usr/bin/perl -wuse strict;use warnings;

use Test::More;

sub add { my (@inputs) = @_; my $result = 0; for my $input ( @inputs ) { $result += $input; } return $result;}

# testing 'add' subroutineis( add(1, 2), 3 );is( add( (1 .. 10) ), 55 );

done_testing();

Subroutine to be tested

Input for test

Expected output

Page 6: あんなテスト、こんなテスト(this and that about testing)

- 6 -Public/公開情報

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)

Page 7: あんなテスト、こんなテスト(this and that about testing)

- 7 -Public/公開情報

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)

Page 8: あんなテスト、こんなテスト(this and that about testing)

- 8 -Public/公開情報

標準入出力(STDIN/STDOUT)

コマンドラインオプション(commandline options)

時刻(system clock)

DB

etc.

Page 9: あんなテスト、こんなテスト(this and that about testing)

Public/公開情報

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

Page 10: あんなテスト、こんなテスト(this and that about testing)

- 10 -Public/公開情報

Automated Testing (2)

Ex 1)using IO::Scalar and capture STDIN

#!/usr/bin/perl -wuse strict;use warnings;use Test::More;

sub add { my $result = 0; while( <STDIN> ) { chomp; $result += $_; } return $result;}

subtest 'add', sub { my $inputs = "1\n2\n3\n"; # input from STDIN open my $stdin_fh, '<', \$inputs; local *STDIN = *$stdin_fh; # replace default STDIN is( add(), 6 );};done_testing();

Page 11: あんなテスト、こんなテスト(this and that about testing)

- 11 -Public/公開情報

Automated Testing (2)

Ex 2) using Capture::Tiny(for STDOUT/STDERR)

#!/usr/bin/perl -wuse 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)

Page 12: あんなテスト、こんなテスト(this and that about testing)

- 12 -Public/公開情報

標準入出力(STDIN/STDOUT)

コマンドラインオプション(commandline options)

時刻(system clock)

DB

etc.

Page 13: あんなテスト、こんなテスト(this and that about testing)

Public/公開情報

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 を書き換える

Page 14: あんなテスト、こんなテスト(this and that about testing)

- 14 -Public/公開情報

Tests for command-line args

Ex) testing command-line args#!/usr/bin/perl -wuse strict;use warnings;use Test::More;our $a_str = undef;# 本当は別 package にある / in real case, this subroutine is defined in other packagesub 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();

Page 15: あんなテスト、こんなテスト(this and that about testing)

- 15 -Public/公開情報

標準入出力(STDIN/STDOUT)

コマンドラインオプション(commandline options)

時刻(system clock)

DB

etc.

Page 16: あんなテスト、こんなテスト(this and that about testing)

Public/公開情報

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)

Page 17: あんなテスト、こんなテスト(this and that about testing)

- 17 -Public/公開情報

Tests for system clock

Ex) Test::MockTime

#!/usr/bin/perluse 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();

Page 18: あんなテスト、こんなテスト(this and that about testing)

- 18 -Public/公開情報

標準入出力(STDIN/STDOUT)

コマンドラインオプション(commandline options)

時刻(system clock)

DB

etc.

Page 19: あんなテスト、こんなテスト(this and that about testing)

Public/公開情報

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

Page 20: あんなテスト、こんなテスト(this and that about testing)

- 20 -Public/公開情報

標準入出力(STDIN/STDOUT)

コマンドラインオプション(commandline options)

時刻(system clock)

DB

etc.

Page 21: あんなテスト、こんなテスト(this and that about testing)

- 21 -Public/公開情報

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)

Page 22: あんなテスト、こんなテスト(this and that about testing)

- 22 -Public/公開情報

Exit measures(2)

Ex1) exit() causes problem

% prove exit.texit.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

#!/usr/bin/perl -wuse 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, '' );

– This test successes unexpectedly

Page 23: あんなテスト、こんなテスト(this and that about testing)

- 23 -Public/公開情報

Exit measures(3)

Ex2) measured exit() call

% prove exit_measured.t exit_measured.t .. 1/? unexpected exit called! at exit_measured.t line 6....(snip)Result: FAIL

#!/usr/bin/perl -wuse 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, '' );

– It's OK. It should be failed.

Page 24: あんなテスト、こんなテスト(this and that about testing)

- 24 -Public/公開情報

setUp/tearDown(1)

xUnit を使っていた人は setUp/tearDown が使いたいかも

(xUnit users may want to use setup/tearDown)

– それ Test::Class で出来るよ!(Test::Class enables it!)

#!/usr/bin/perl -wuse 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 }

Page 25: あんなテスト、こんなテスト(this and that about testing)

- 25 -Public/公開情報

setUp/tearDown(2)

でも書き方が変わるのは面倒くさい

(But it isn't good that how to write test is changed)

– subtest + Hook::LexWrap#!/usr/bin/perl -wuse 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();

Page 26: あんなテスト、こんなテスト(this and that about testing)

- 26 -Public/公開情報

setUp/tearDown(3)

実行例(execution example)

% perl setup_teardown.t# setup ok 1 1..1ok 1 - my_test# teardown# setup ok 1 1..1ok 2 - my_test2# teardown1..2

setup called

test called

teardown called

Page 27: あんなテスト、こんなテスト(this and that about testing)

- 27 -Public/公開情報

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)

Page 28: あんなテスト、こんなテスト(this and that about testing)

- 28 -Public/公開情報

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)

Page 29: あんなテスト、こんなテスト(this and that about testing)

- 29 -Public/公開情報

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)

Page 30: あんなテスト、こんなテスト(this and that about testing)

- 30 -Public/公開情報

Page 31: あんなテスト、こんなテスト(this and that about testing)

- 31 -Public/公開情報

質疑をうけて、ちょっとだけ補足(発表後に追記)

exit で意図せずテストが成功する場合の話ですが、これは「no_plan」にしているときのみ発生する事象です。

比較的新しい Test::More を使っていれば、done_testing() が使えるので、それを使うべきです。また、5.8.8 とかに標準添付される Test::More だと done_testing()が使えないバージョンなので、その場合は no_plan をそもそも避けるべきです。

(sorry only in Japanese)