Go Conference’19 Summer in Fukuoka で費用対効果の良いテストを目指すための目的・基礎・実践について話したものです
© - BASE, Inc. XCost-effective Go Unit Testthinking and practicesGo Conference’ Summer in Fukuoka. . - @hgsgtk
View Slide
© - BASE, Inc.Talk StructureCost-effective Unit TestGo Unit Test BasicReal Practices in GoWHYBASICPRACTICE
© - BASE, Inc.: @hgsgtk⾃⼰紹介東⼝ 和暉 ( Higashiguchi Kazuki )Go, PHP, PythonBASE BANK, Inc. (BASE, Inc.)/ Dev Division/ Tech Lead
© - BASE, Inc.Cost-effective Unit Test
© - BASE, Inc.Unit Testとは• プログラムを構成する⽐較的⼩さな単位(ユニット)が個々の機能を正しく果たしているかどうかを検証する• 実⾏⼿段として、⼿動によるテスト‧⾃動化テストが存在する• このトークでは、⾃動化テストを取り上げる
© - BASE, Inc.WHY? なぜUnit Testを書くのか• ⽋陥混⼊の阻⽌、バグを防ぎたい?• テストによるドキュメンテーション?• 設計改善の指標?
© - BASE, Inc.このトークで定義する⽬的コスト削減
© - BASE, Inc.コスト削減の指標Unit TestʹΑΔίετݮUnit Testͷ࡞ɾҡ࣋ίετVS
© - BASE, Inc.• ৽نςετ࡞ίετൃੜ• طଘςετҡ࣋ίετൃੜ• ςετ࣮ߦ࣌ؒͷͪίετൃੜ• ࣗಈςετͷͨΊͷCIҡ࣋ίετൃੜ• Ϣχοτςετͷֶशίετൃੜユニットテストの両⾯のコストUnit TestʹΑΔίετݮUnit Testͷ࡞ɾҡ࣋ίετVS• खಈϢχοτςετͷίετݮ• ܽؕͷૣظൃݟʹΑΔमਖ਼ίετݮ• υΩϡϝϯςʔγϣϯίετݮ• σόοάίετݮ• ઃܭվળʹΑΔϝϯςφϯείετݮ
© - BASE, Inc.• ৽نςετ࡞ίετൃੜ• طଘςετҡ࣋ίετൃੜ• ςετ࣮ߦ࣌ؒͷͪίετൃੜ• ࣗಈςετͷͨΊͷCIҡ࣋ίετൃੜ• Ϣχοτςετͷֶशίετൃੜコストへの集約Unit TestʹΑΔίετݮUnit Testͷ࡞ɾҡ࣋ίετVS• खಈϢχοτςετͷίετݮ•ܽؕͷૣظൃݟʹΑΔमਖ਼ίετݮ• υΩϡϝϯςʔγϣϯίετݮ• σόοάίετݮ• ઃܭվળʹΑΔϝϯςφϯείετݮ⽋陥混⼊の阻⽌、バグを防ぎたい?
© - BASE, Inc.• ৽نςετ࡞ίετൃੜ• طଘςετҡ࣋ίετൃੜ• ςετ࣮ߦ࣌ؒͷͪίετൃੜ• ࣗಈςετͷͨΊͷCIҡ࣋ίετൃੜ• ϢχοτςετͷֶशίετൃੜUnit TestʹΑΔίετݮUnit Testͷ࡞ɾҡ࣋ίετVSテストによるドキュメンテーション?• खಈϢχοτςετͷίετݮ• ܽؕͷૣظൃݟʹΑΔमਖ਼ίετݮ•υΩϡϝϯςʔγϣϯίετݮ• σόοάίετݮ• ઃܭվળʹΑΔϝϯςφϯείετݮコストへの集約
© - BASE, Inc.• ৽نςετ࡞ίετൃੜ• طଘςετҡ࣋ίετൃੜ• ςετ࣮ߦ࣌ؒͷͪίετൃੜ• ࣗಈςετͷͨΊͷCIҡ࣋ίετൃੜ• ϢχοτςετͷֶशίετൃੜUnit TestʹΑΔίετݮUnit Testͷ࡞ɾҡ࣋ίετVS設計改善の指標?• खಈϢχοτςετͷίετݮ• ܽؕͷૣظൃݟʹΑΔमਖ਼ίετݮ• υΩϡϝϯςʔγϣϯίετݮ• σόοάίετݮ•ઃܭվળʹΑΔϝϯςφϯείετݮコストへの集約
© - BASE, Inc.Cost effective Unit TestUnit TestʹΑΔίετݮUnit Testͷ࡞ɾҡ࣋ίετ>
© - BASE, Inc.See alsohttps://speakerdeck.com/hgsgtk/practices-to-write-better-unit-test
© - BASE, Inc.BASIC Go Unit Test Basic• 適切なエラーレポート• 適切なエラーハンドリング• テーブル駆動テスト‧サブテストBASIC
© - BASE, Inc.例題:不適切なエラーレポート
© - BASE, Inc.例題:不適切なエラーレポート=== RUN TestInStatusListInAppropriateErrorReport--- FAIL: TestInStatusListInAppropriateErrorReport ( . s)simple_test.go: : unexpected value truesimple_test.go: : unexpected value trueFAILFAIL github.com/hgsgtk/go-snippets/testing-codes/simple. s
© - BASE, Inc.不適切なエラーレポートの弊害. 「どこで何を検証して失敗したのか」が読みとりにくい. ⼊⼒‧期待値が結果から読み取れない. 問題が起きたか知るためにテストコード‧動作コードを読む必要がある--- FAIL: TestInStatusListInAppropriateErrorReport ( . s)simple_test.go: : unexpected value true
© - BASE, Inc.適切なエラーレポートとはGo FAQ “Why does Go not have assertions?“Refs: https://golang.org/doc/faq#assertions“Proper error reporting means that errors aredirect and to the point, saving theprogrammer from interpreting a large crashtrace.”直接的 かつ 適切
© - BASE, Inc.例題を改善する
© - BASE, Inc.例題を改善する直接的かつ適切なエラーレポートを⾏うex. “f(x) = y, want z”という⼀つの形式f(x): 試みた結果と⼊⼒y: 得られた結果z: 期待値
© - BASE, Inc.例題を改善する=== RUN TestInStatusListProperErrorReport--- FAIL: TestInStatusListProperErrorReport ( . s)simple_test.go: : InStatusList(`unknown `) = true, want falsesimple_test.go: : InStatusList(`unknown `) = true, want falseFAILFAIL github.com/hgsgtk/go-snippets/testing-codes/simple
© - BASE, Inc.• ৽نςετ࡞ίετൃੜ•طଘςετҡ࣋ίετൃੜ• ςετ࣮ߦ࣌ؒͷͪίετൃੜ• ࣗಈςετͷͨΊͷCIҡ࣋ίετൃੜ• Ϣχοτςετͷֶशίετൃੜ適切なエラーレポートの効果Unit TestʹΑΔίετݮUnit Testͷ࡞ɾҡ࣋ίετVS• खಈϢχοτςετͷίετݮ• ܽؕͷૣظൃݟʹΑΔमਖ਼ίετݮ• υΩϡϝϯςʔγϣϯίετݮ• σόοάίετݮ• ઃܭվળʹΑΔϝϯςφϯείετݮテスト失敗時の原因調査コストの削減
© - BASE, Inc.適切なエラーハンドリングとは“Proper error handling means that serverscontinue to operate instead of crashing after anon-fatal error.”致命的ではないエラーの場合はクラッシュせずに処理を継続するGo FAQ “Why does Go not have assertions?“Refs: https://golang.org/doc/faq#assertions
© - BASE, Inc.適切なエラーハンドリング
© - BASE, Inc.適切なエラーハンドリングここで失敗すると後続の検証が不可なので致命的として、testing.Fatalf()でハンドリング
© - BASE, Inc.適切なエラーハンドリング後続の処理可能なため t.Errorf() でハンドリングする
© - BASE, Inc.• ৽نςετ࡞ίετൃੜ•طଘςετҡ࣋ίετൃੜ• ςετ࣮ߦ࣌ؒͷͪίετൃੜ• ࣗಈςετͷͨΊͷCIҡ࣋ίετൃੜ• Ϣχοτςετͷֶशίετൃੜ適切なエラーハンドリングの効果Unit TestʹΑΔίετݮUnit Testͷ࡞ɾҡ࣋ίετVS• खಈϢχοτςετͷίετݮ• ܽؕͷૣظൃݟʹΑΔमਖ਼ίετݮ• υΩϡϝϯςʔγϣϯίετݮ• σόοάίετݮ• ઃܭվળʹΑΔϝϯςφϯείετݮ致命的なもののみをcrashさせるため、継続可能な限り、⼀回の実⾏で多く検証結果を得られる
© - BASE, Inc.テーブル駆動テストとは“Each table entry is a complete test case withinputs and expected results, and sometimeswith additional information such as a testname to make the test output easily readable.”⼊⼒と期待値を含むテストエントリRefs: https://github.com/golang/go/wiki/TableDrivenTests
© - BASE, Inc.テーブル駆動テスト‧サブテスト
© - BASE, Inc.テーブル駆動テスト‧サブテスト⼊⼒と期待値を含むテストエントリ
© - BASE, Inc.テーブル駆動テスト‧サブテストtesting.Run()でサブテストとして実⾏-run で特定ケースを実⾏できるgo test ./... -v -run TestGetMsgSubTest/divisible_by_
© - BASE, Inc.•৽نςετ࡞ίετൃੜ•طଘςετҡ࣋ίετൃੜ• ςετ࣮ߦ࣌ؒͷͪίετൃੜ• ࣗಈςετͷͨΊͷCIҡ࣋ίετൃੜ• Ϣχοτςετͷֶशίετൃੜテーブル駆動テスト‧サブテストの効果Unit TestʹΑΔίετݮUnit Testͷ࡞ɾҡ࣋ίετVS• खಈϢχοτςετͷίετݮ• ܽؕͷૣظൃݟʹΑΔमਖ਼ίετݮ• υΩϡϝϯςʔγϣϯίετݮ•σόοάίετݮ• ઃܭվળʹΑΔϝϯςφϯείετݮサブテストによる特定テストケースの実⾏
© - BASE, Inc.•৽نςετ࡞ίετൃੜ•طଘςετҡ࣋ίετൃੜ• ςετ࣮ߦ࣌ؒͷͪίετൃੜ• ࣗಈςετͷͨΊͷCIҡ࣋ίετൃੜ• Ϣχοτςετͷֶशίετൃੜテーブル駆動テスト‧サブテストの効果Unit TestʹΑΔίετݮUnit Testͷ࡞ɾҡ࣋ίετVS• खಈϢχοτςετͷίετݮ• ܽؕͷૣظൃݟʹΑΔमਖ਼ίετݮ• υΩϡϝϯςʔγϣϯίετݮ•σόοάίετݮ• ઃܭվળʹΑΔϝϯςφϯείετݮテストパターンと検証ロジックの分離の効果同じ検証ロジックを何度も書かずに済む
© - BASE, Inc.•৽نςετ࡞ίετൃੜ•طଘςετҡ࣋ίετൃੜ• ςετ࣮ߦ࣌ؒͷͪίετൃੜ• ࣗಈςετͷͨΊͷCIҡ࣋ίετൃੜ• Ϣχοτςετͷֶशίετൃੜテーブル駆動テスト‧サブテストの効果Unit TestʹΑΔίετݮUnit Testͷ࡞ɾҡ࣋ίετVS• खಈϢχοτςετͷίετݮ• ܽؕͷૣظൃݟʹΑΔमਖ਼ίετݮ• υΩϡϝϯςʔγϣϯίετݮ•σόοάίετݮ• ઃܭվળʹΑΔϝϯςφϯείετݮエントリ追加でテストのバリエーションを修正できる
© - BASE, Inc.PRACTICE Real Practices in GoPRACTICEBASICWHYカスタムアサーション テストダブルを使うテストRDBMSを使う処理のテスト
© - BASE, Inc.起きがちな課題:Copy & Paste
© - BASE, Inc.テストコードが増えてくると• HTTP Handlerなど関⼼事が領域が近い処理のテストコードは、同じ関⼼領域を類似した検証ロジックでテストする• コピペが⽣まれると往々にして「適切なエラーレポート」が忘れられる• ⇒ テストの維持コストの増加
© - BASE, Inc.共通処理を⾏うテストヘルパーの選択肢• 同様の関⼼‧ユースケースがある処理は testutil パッケージなどにして共通化する• ⇒ 新規テストコストの削減
© - BASE, Inc.実例:HTTP Handlerのアサーションを共通処理にする• t: *testing.T を求め、t.Helper()をcall• testTarget: 「どのHandlerを施⾏するか」を設定• gotRes: 試⾏結果の *http.Response• wantCode, bodyFile: 期待値
© - BASE, Inc.実例:HTTP Handlerのアサーションを共通処理にするAssertResponse()をcallJSONのresponse bodyの期待値は別ファイルで配置
© - BASE, Inc.実例:HTTP Handlerのアサーションを共通処理にする• testdata ディレクトリを作成• ⇒パッケージとしてみなされない、テスト時のみ必要なファイルであることが明⽰的になる• *.json.golden 拡張⼦として配置• ⇒ 標準パッケージでも使われている⼿法• ⇒テストのドキュメントとしての価値の向上
© - BASE, Inc.実例:HTTP Handlerのアサーションを共通処理にする
© - BASE, Inc.実例:HTTP Handlerのアサーションを共通処理にする別ファイル定義したJSONファイルと⽐較⽐較には google/go-cmp を利⽤
© - BASE, Inc.テストダブルTest Double: テスト固有の同等物Mock: テスト対象に適切に使⽤されているか検証するRefs ॻ੶ʰxUnit Test Patterns: Refactoring Test Codeʱ/ Chapter 23. Test Double Patterns
© - BASE, Inc.※補⾜:このトーク内での “Mock”Refs ॻ੶ʰςετۦಈ։ൃʱ/ C ϢχοτςετपลͷࣝͷཧͱTDD֦ுͷࢼΈ• xUTPの語彙整理により、「広義のMockObject」‧「狭義のMock Object」を分けて議論できるようになった• このトークでの “Mock” もTest Doubleの範囲をざっくりと含める「広義のMock Object」とする
© - BASE, Inc.テストダブルを使うテスト次のような属性を持つものに依存する場合に、テスト時にテストダブルに置き換える事がある• 結果が不定• ex: UUID• システム外コンポーネントを利⽤する処理• ex: API, RDBMS, AWS etc• レイヤ構造をまたぐ依存• ex: Service->Repository
© - BASE, Inc.テストダブル作成の選択肢としてのgomock• “GoMock is a mocking frameworkfor the Go programming language.”• https://github.com/golang/mock
© - BASE, Inc.例: gomockを利⽤するテスト例
© - BASE, Inc.例: gomockを利⽤するテスト例gomockで依存関係のあるInterfaceを実装したモックを作成Times()をcallして、呼び出される期待を設定する=> 狭義のMock
© - BASE, Inc.この例の問題点• すべての依存関係に呼び出しの期待を設定している• 外の振る舞いを変えない内部構造の変更に敏感に反応する• ⇒ リファクタリングの障害になる
© - BASE, Inc.的確なエクスペクテーションRefs ॻ੶ʰ࣮ફςετۦಈ։ൃʱ / ୈ20ষ ςετͷΛௌ͘“意図を明確にするためには、スタブとエクスペクテーション‧アサーションをきっちり区別するとよい。”
© - BASE, Inc.区別するための指標:コマンド/クエリRefs ॻ੶ʰ࣮ફςετۦಈ։ൃʱ / ୈ20ষ ςετͷΛௌ͘ΫΤϦʹΞϩʔΞϯεɺίϚϯυʹΤΫεϖΫςʔγϣϯ• コマンドとは、 副作⽤を伴うことのある呼び出し• オブジェクトの外の世界を変化させる• ex. DBのレコード保存‧状態変化• 複数回実⾏した場合、システム状態は違ったものになる => エクスペクテーション
© - BASE, Inc.区別するための指標:コマンド/クエリ• クエリは、外の世界を変化させない• 何回呼んでも呼ばなくても構わない• ⇒ アローアンスRefs ॻ੶ʰ࣮ફςετۦಈ։ൃʱ / ୈ20ষ ςετͷΛௌ͘ΫΤϦʹΞϩʔΞϯεɺίϚϯυʹΤΫεϖΫςʔγϣϯ
© - BASE, Inc.改善する例
© - BASE, Inc.改善する例Anytimes()をcallすることで「呼んでも呼ばなくてもいい」Stubになる
© - BASE, Inc.RDBMSを使う処理の例
© - BASE, Inc.RDBMSを使う処理のテストの選択肢• 期待したSQLを発⾏するかを検証する• 実際のDBは含めない、DBとの協調が期待通りか• ex: DATA-DOG/go-sqlmockの利⽤• 実際のDBも検証範囲に含める• データベースを起動しテストに利⽤する• ex: lestrrat-go/test-mysqldの利⽤
© - BASE, Inc.実感値: 期待したSQLを発⾏するかを検証する⽅法• Props• テスト速度が速い• Cons• ⽋陥の発⽣可能性、期待値のSQLが間違っている• SQLの結果⾃体が責務の⼤部分を占める場合に「SQLが正しい結果を得るものかどうか」を確かめにくい
© - BASE, Inc.実感値: 実際のDBも検証範囲に含める• Props• ⽋陥の発⽣可能性は下がる、 “安⼼感” も⾼い• 単体テストをデバッグに使うスタイルの場合、DBを使えるのでやりやすい• Cons• テスト速度は遅い• 実際のDBを使えるようにするまでの準備はそれなりに⼤変
© - BASE, Inc.The Test Automation PyramidUIServiceUnitRefs ॻ੶ʰSucceeding with Agileʱ / Chapter.16 Qualityスコープの拡⼤⾃信の向上⾼速分離性の向上
© - BASE, Inc.• ৽نςετ࡞ίετൃੜ• طଘςετҡ࣋ίετൃੜ•ςετ࣮ߦ࣌ؒͷͪίετൃੜ• ࣗಈςετͷͨΊͷCIҡ࣋ίετൃੜ• Ϣχοτςετͷֶशίετൃੜ⽐較:RDBMSを使う処理のテストの選択肢Unit TestʹΑΔίετݮUnit Testͷ࡞ɾҡ࣋ίετVS• खಈϢχοτςετͷίετݮ•ܽؕͷૣظൃݟʹΑΔमਖ਼ίετݮ• υΩϡϝϯςʔγϣϯίετݮ•σόοάίετݮ• ઃܭվળʹΑΔϝϯςφϯείετݮ実際のDBも検証範囲に含める期待したSQLを発⾏するかを検証する⽅法
© - BASE, Inc.See also 期待したSQLを発⾏するかを検証するRefs https://devblog.thebase.in/entry/2018/12/04/110000
© - BASE, Inc.See also 実際のDBも検証範囲に含めるRefs https://medium.com/@timakin/go-api-testing-173b97fb23ec• timakin さんのこちらの記事が参考になります。• 「GoのAPIのテストにおける共通処理」• https://medium.com/@timakin/go-api-testing- b fb ec
© - BASE, Inc.まとめ• 費⽤対効果の良いテストを⽬指す• 費⽤対効果のいいテストを⽬指す考えかたとして、Goの考え⽅は有効• 実際の現場での試⾏錯誤も、⽬的‧基礎を腹に持っておくといい
© - BASE, Inc.fin.