Upgrade to Pro — share decks privately, control downloads, hide ads and more …

スキップしていいテスト、スキップしてはいけないテスト 〜速さと信頼を兼ねたテストコードを構築す...

mackee
July 01, 2017

スキップしていいテスト、スキップしてはいけないテスト 〜速さと信頼を兼ねたテストコードを構築する術〜 / Need for speed of testing in Perl5 Web Application.

YAPC::Fukuoka 2017 HAKATA

mackee

July 01, 2017
Tweet

More Decks by mackee

Other Decks in Programming

Transcript

  1. 誰 macopy/ マコピー github: @mackee twitter: @mackee_w PAUSE: MACOPY App::Prove::Plugin::MySQLPool

    language: 日本語, Perl5, Go kuiperbelt - WebSocket と普通のHTTP/1.1 を変換してくれる君 興味: リアルタイム通信, ワークフロー, ORM, MySQL Yokohama.pm Co-Organizer 最近: 3D プリンタ, altcoin 掘削 2 / 58
  2. これは何の話 さてみなさんテストを書いていますか? そして継続的にテストを実行していますか? テストを書かない、またはCI を回さない理由としては以下のような物が考えられます。 - めんどくさい(わかる - テストの実行時間が長い(わかる -

    テストがコケたりコケなかったりする(わかる - テスト回してコケたらデプロイが出来ないルールとかだと デプロイがいつまで経ってもできない(わかる - そもそも人力テスト(QA など) やってるし、要らんやろ(わかる - コード書いた人間が想定する壊れそうな部分しか テスト書かれないんだから意味ないやろ(わかる というなどなど挙げられます。 このトークでは、未来へ継続していくWeb サービスアプリケーションを開発・運用する上で __ あえて__ テストをスキップする話や、テスト 4 / 58
  3. 現状 リリース頻度 1 日1 回~2 回 ただしデプロイを避けたい時間帯がある GvG の時間帯 1

    日4 回 合計10 時間 イベントの定期実行バッチ時間 主に昼間の4 時間が狙い目 フルテスト実行にかかる時間 30 分 リリース初期は7 分ぐらいだったんだ…… 機能追加やらバグってたケースの追加とかで膨らむ デプロイフェーズ直前のテスト失敗は修正も含めて3 度ぐらいしか出来ない 16 / 58
  4. 現状 ほとんどの機能やマスタデータはtopic ブランチを経由して、master ブランチにマージされ、更にrelease ブラン チに入れている つまりtopic とmaster とrelease で3

    回フルテストが必要 master とrelease に入れるときのPullRequest には2 人以上のレビューが必要なルールにしている レビューも時間がかかるんや…… 17 / 58
  5. 甲子園ポケットのテスト側の仕組み DB のスキーマはワーカーごとにDDL を生成して食わせる サブテスト内のDB への変更はサブテスト終了時にクリーンアップする よく見るトランザクション使ってロールバックではなく全テーブルをTRUNCATE している 外側でトランザクションを貼るのはロールバックする場合のテストとかが出来なくて本番と同様の挙動を再 現できないため

    # apptest は内部でsubtest を実行しつつ前後でDB をクリーンアップしたりlogger のモック化やら時間の固定化などをしている apptest " なんとかのテスト" => sub { # create_user_model はユーザ登録に必要なマスタデータがなければ作ってその上で実際のユーザ登録メソッドを叩いてモデルを作る my $user_model = create_user_model; }; 21 / 58
  6. テストコード内ではよくcreate_user_model が使われてい る こういうのが $ find t/ -name \*.t |

    xargs cat | grep 'create_user_model' | wc -l 3647 こんな感じ 36 個のテーブルのINSERT をしていると思うとゾッとしますね、はい # apptest は内部でsubtest を実行しつつ前後でDB をクリーンアップしたりlogger のモック化やら時間の固定化などをしている apptest " なんとかのテスト" => sub { # create_user_model はユーザ登録に必要なマスタデータがなければ作ってその上で実際のユーザ登録メソッドを叩いてモデルを作る my $user_model = create_user_model; }; 27 / 58
  7. A: 一気にINSERT する my $mysqld = Test::mysqld->new( my_cnf => my_cnf(),

    ); my $sql_file = test_ddl_file(); $sql_file->remove; mysql_prepare($mysqld); mysql_override_config(); create_user_model for 1..$self->user_num; my $opts = mysqld_opts($mysqld); my $output_path = $sql_file->stringify; system(qq{mysqldump --no-create-info $opts test > $output_path}); 30 / 58
  8. A: 一気にINSERT する 既存のテストでPK 指定でINSERT するやつとduplicate して死ぬ そもそもPK 指定でINSERT するな!(ID

    に依存したテスト書かない。。。) # apptest が呼ばれた時点でSQL ファイルが全部食われる apptest " ほげテスト" => sub { # このテーブルのid=1 のやつはすでに作ったSQL で作られているのでduplicate する create_dummy("FugaTable", id => 1); }; truncate_all という便利関数がせっかく作ったダミーデータを全て洗い流してくれるのでduplicate しない。 apptest " ほげテスト" => sub { truncate_all; # 全部消されるから便利…便利? create_dummy("FugaTable", id => 1); }; とりあえずコケたやつでこのケースであればこれで対処した 32 / 58
  9. インスタンスサイズの拡大と並列度を増やす 過去: r3.8xlarge or cc2.8xlarge 32 CPU prove -j 16

    現在: i3.16xlarge やm4.16xlarge など64 CPU のものから安いやつを選ぶ prove -j 80 39 / 58
  10. 次の改善 -> 同じテストを何度もやる問題 topic -> master -> release... topic までは分かる、master

    とrelease はhotfix がない限りはだいたい一緒 都度10 分かかるのはどうか 41 / 58
  11. Table Driven Test TableDrivenTests - github.com/golang/go Go で知った話だけれどPerl でも出来る Example:

    note " 現在の連続ヒット数を計算する関数のテスト"; my @cases = ( { scores => [hit_single(), hit_double()], expect => 2, note => "2 連続ヒット" }, { scores => [hit_single(), bounder_out()], expect => 0, note => " ヒット後にゴロアウト" }, { scores => [bounder_out(), hit_single()], expect => 1, note => " ゴロアウト語にヒット" }, ); for my $case (@cases) { subtest $case->{note} => sub { my $inning = MyApp::Inning->new(scores => $case->{scores}); is $inning->hitting_streak, $note->{expect}; }; } 46 / 58
  12. かつては地道に書いていた MyApp::DBImporter->from_csv([qw/item item_effect shop_item/]); my $item_rs = MyApp::DB::Item->search; while (my

    $item = $item_rs->next) { ok $item->item_effect, " リレーション先のitem_effect があるかどうか"; if ($item->is_buyable) { ok $item->shop_item, " 購入可能なアイテムの場合、有効なショップマスタにアイテムが有るか"; } } 49 / 58
  13. というわけでこんな感じになりました subtest "dungeon" => sub { # dungeon.event_id と同じdungeon_floor.event_id があるかどうか

    has_relation "dungeon", "dungeon_floor", id => "dungeon_id"; # dungeon_floor.battle_times が1 から10 回以内か expect_row "dungeon_floor" => "battle_times", sub { my $battle_times = shift; return 1 <= $battle_times && $battle_times <= 10; }; # dungeon_floor.dungeon_floor_monster_odds に対応する # dungeon_floor_monster.odds があるかどうか has_relation "dungeon_floor" => "dungeon_floor_monster", dungeon_floor_monster_odds => "odds"; }; 53 / 58
  14. Mock/Stub の話 Test::Mock::Guard 使っている 速さの面で言うとstub すると重い部分をスキップできる可能性がある my $g = mock_guard("HeavyModel"

    => { heavy_method => sub { return 1; }, }); ok CallerModel->new(heavy_model => HeavyModel->new)->call_heavy_method; しかし果たしてそれはテストになっているのか? stub はテストされるコードとテストコードを曖昧にする 本来テストしたい挙動が歪められる可能性がある テストを書く側はstub するメソッドなどがコード側で呼ばれていることを知っていないとstub 出来ない ブラックボックステストが出来ない 55 / 58
  15. N+1 を防ぐためにテストのときだけメソッドモディフ ァイア的なことをしてprefetch されてなければ死ぬ my $orig = MyApp::Model::Player->can("player_info_with_more_info"); my $guard

    = mock_guard "MyApp::Model::Player" => { player_info_with_more_info => sub { my ($self) = @_; if ( !exists $self->player->{_relationship_data}{player_more_info} && $self->player->in_storage ) { confess "method called without prefetching player_more_info"; } $orig->(@_); }, }; 57 / 58