Slide 1

Slide 1 text

自動テストのないプロダクトの 開発効率化への道 SWETグループ 金子淳貴

Slide 2

Slide 2 text

自己紹介 ● 氏名:金子淳貴(@theoden9014) ● 所属:DeNA SWETグループ ● 業務内容: ○ Go言語プロダクトの開発効率化と品質向上 ○ ゲーム開発プロセス改善 ○ etc...

Slide 3

Slide 3 text

この発表のゴール ● 自動テストが入ってないプロダクトを改善する大きな流れ ● サーバアプリに自動テストを入れるための最初の一歩

Slide 4

Slide 4 text

アジェンダ ● 自動テストがないプロダクトの課題 ● 後から自動テストを導入するには? ● データベースを利用した大きめのテスト

Slide 5

Slide 5 text

アジェンダ ● 自動テストがないプロダクトの課題 ● 後から自動テストを導入するには? ● データベースを利用した大きめのテスト

Slide 6

Slide 6 text

自動テストが無いプロダクトの課題 収益性や生産性が目標レベルを上回る確率は ハイパフォーマーはローパフォーマーの2倍 LeanとDevOpsの科学 付録B 統計データより ● 開発者が手元で動作確認 ● 一度開発したところが動かなくなる ● 「手戻り」が発生しやすい、etc... 効率が悪い 自動テストを導入すると 開発効率が上がる!

Slide 7

Slide 7 text

自動テストは簡単に導入できるのか? ● プロダクトの設計次第では、後から自動テストを 入れるのに大きなコストがかかる ● 以下のような設計だと自動テストを導入しにくい ○ 疎結合アーキテクチャになっておらず ロジックが動作する際に統合環境を必要とする ■ 例:ビジネスロジックとDB処理を切り離せない

Slide 8

Slide 8 text

自動テストの分類 - Test Size 参考文献:https://bit.ly/3nYqtgn Network Access Database File System Access Use external system Multiple threads Sleep Statements System properties Time limit (sec) Small No No No No No No No 60 Medium localhost Yes Yes Discouraged Yes Yes Yes 300 Large Yes Yes Yes Yes Yes Yes Yes 900+ テスト実行時にシステムやアプリが依存している 特性に基づいて、テストの大きさを3つに分類

Slide 9

Slide 9 text

● 実装コスト ● メンテナンスコスト Test Sizeのコストパフォーマンス Small Medium Large High Low テストピラミッド このような比重で 自動テストを実装すると コストパフォーマンスが良い

Slide 10

Slide 10 text

理想は最初からSmall Testを書けるように ロジックを疎結合にしておく ● 設計の段階でロジックを疎結合にしておく ○ Layered Architecture ○ Dependency Injection ○ etc ....

Slide 11

Slide 11 text

アジェンダ ● 自動テストがないプロダクトの課題 ● 後から自動テストを導入するには? ● データベースを利用した大きめのテスト

Slide 12

Slide 12 text

後から自動テストを入れることができるのか? ● Small Testを入れるには設計、実装の段階で アプリケーションを疎結合にしておく必要がある ● そうでない時でも、リファクタリングすることで Small Testを入れられるようにすることはできる

Slide 13

Slide 13 text

リファクタリングの流れ - Pinning Tests 1. 外から見た動作を変えないように 大きめのテスト(Medium や Large Test)を入れる 2. そのテストが担保している範囲でアプリケーショ ンの疎結合の度合いを上げるようなリファクタリ ングを実施 3. Small Testを入れてより粒度の高い確認をする 具体的には「レガシーコードからの脱却」を参照 https://www.oreilly.co.jp/books/9784873118864/

Slide 14

Slide 14 text

アジェンダ ● 自動テストがないプロダクトの課題 ● 後から自動テストを導入するには? ● データベースを利用した大きめのテスト

Slide 15

Slide 15 text

大きめのテスト ● 今回は一般的なWebシステムであれば必ずと言っ ていいほど利用されているデータベースを用いて いるシステムを対象 ● 具体的な事例を踏まえて紹介

Slide 16

Slide 16 text

今回の事例 - 概要 ● 自動テストが全く入っていない開発後期のプロダ クトに対して自動テストを導入 ● Go言語で実装されているAPIサーバ ● DBと切り離してテストすることが難しい ● テーブルの数は200+

Slide 17

Slide 17 text

今回の事例 - 要件 ● テーブル駆動テストでわかりやすくしたい ○ テストデータをコードで定義 ○ 特定の状態のデータは使いまわしたい (例: BANされたUser) ● テストは使い捨てではなく今後も使い続ける予定 ○ テストはメンテナンスしやすい状態にしたい ● データベースにCloud Spannerを利用 ○ 内製のORMを利用 ○ Truncateコマンドが存在しない ○ 外部キー制約を無効にできない

Slide 18

Slide 18 text

テーブル駆動テストとは テストの入出力をテーブルで管理し、 テストケースの網羅性を上げる為の手法 The Go Blog: https://blog.golang.org/subtests 自動生成ツール: https://github.com/cweill/gotests

Slide 19

Slide 19 text

どうやって自動テストする? ● テスト環境用にDBを新たに構築 ● DBの中身を全てテスト用データに置き換える ● ロジックを自動テストする ● DBの中身を空にする

Slide 20

Slide 20 text

データベースを利用したテストの流れ 1. データベースの Setup 2. テストデータの Setup 3. アプリケーションの Setup 4. アプリケーションの実行 5. テストアサーション 6. アプリケーションの Teardown 7. テストデータの Teardown 8. データベースの Teardown テストデータとテストケース をどうやって管理するの か? テストデータをどうやって削 除するのか?

Slide 21

Slide 21 text

テストデータの管理で考えるべきこと ● テストデータの生成方法は? ● どこまでテストデータの共通化するのか? ● どうやってテストデータを削除するのか?

Slide 22

Slide 22 text

テストデータの管理で考えるべきこと ● テストデータの生成方法は? ● どこまでテストデータの共通化するのか? ● どうやってテストデータを削除するのか?

Slide 23

Slide 23 text

テストデータの生成方法 ● 静的な生成 ○ SQLファイルで管理 ○ データフォーマットを基に生成 ■ github.com/go-testfixtures/testfixtures ● 動的な生成(ORMを利用前提) ○ Goの構造体を基に生成 ■ github.com/bxcodec/faker ■ github.com/bluele/factory-go

Slide 24

Slide 24 text

テストデータの生成方法 ● 静的な生成 ○ SQLファイルで管理 ○ データフォーマットを基に生成 ■ github.com/go-testfixtures/testfixtures ● 動的な生成(ORMを利用前提) ○ Goの構造体を基に生成 ■ github.com/bxcodec/faker ■ github.com/bluele/factory-go 今回はコードでテストデー タを管理したい

Slide 25

Slide 25 text

テストデータの管理で考えるべきこと ● テストデータの生成方法は? ● どこまでテストデータの共通化するのか? ● どうやってテストデータを削除するのか?

Slide 26

Slide 26 text

どこまでテストデータを共通化するのか? テストデータを共通化すると テスト間で相互作用が発生する可能性がある ● 例)テストデータを変えると関係のないテストまで落ちるよう   になってしまう 共通化するとメンテナンスのしやすさは低下する⇒ 今回は基本共通化はしない 詳細は”xUnit Test Patterns: Refactoring Test Code” 今回はメンテナンスし易 い状態にしたい

Slide 27

Slide 27 text

[再掲]今回の事例 - 要件 ● テーブル駆動テストでわかりやすくしたい ○ テストデータをコード内で定義 ○ 特定の状態のデータは使いまわしたい (例: BANされたUser) ● テストは使い捨てではなく今後も使い続ける予定 ○ テストはメンテナンスしやすい状態にしたい ● データベースにCloud Spannerを利用 ○ 内製のORMを利用 ○ Truncateコマンドが存在しない ○ 外部キー制約を無効にできない

Slide 28

Slide 28 text

[再掲]今回の事例 - 要件 ● テーブル駆動テストでわかりやすくしたい ○ テストデータをコード内で定義 ○ 特定の状態のデータは使いまわしたい (例: BANされたUser) ● テストは使い捨てではなく今後も使い続ける予定 ○ テストはメンテナンスしやすい状態であるべき ● データベースにCloud Spannerを利用 ○ 内製のORMを利用 ○ Truncateコマンドが存在しない ○ 外部キー制約を無効にできない テストデータ、基本は共通化しないが 特定の状態を再利用できるようにする

Slide 29

Slide 29 text

テストデータの管理で考えるべきこと ● テストデータの生成方法は? ● どこまでテストデータの共通化するのか? ● どうやってテストデータを削除するのか?

Slide 30

Slide 30 text

どうやってテストデータを削除するのか? ● テスト毎にテーブルのデータを全て削除する ○ Table Truncation Teardown ● Transactionを意図的に失敗させてロールバックする ○ Transaction Rollback Teardown

Slide 31

Slide 31 text

どうやってテストデータを削除するのか? ● テスト毎にテーブルのデータを全て削除する ○ Table Truncation Teardown ● Transactionを意図的に失敗させてロールバックする ○ Transaction Rollback Teardown Spannerには Truncateコマンドがない

Slide 32

Slide 32 text

今回の事例 - テストデータの管理方針 ● テストデータの生成方法は? ○ => Goのコード内 ● どこまでテストデータの共通化をするのか? ○ => 基本しない、どうしても共通化する場合は最小限 ○ => 特定の状態のデータは共有したい ● どうやってテストデータを削除するのか? ○ => Transaction Rollback Teardown

Slide 33

Slide 33 text

[再掲]今回の事例 - 要件 ● テーブル駆動テストでわかりやすくしたい ○ コード内で定義 ○ 特定の状態のデータは使いまわしたい (例: BANされたUser) ● テストは使い捨てではなく今後も使い続ける予定 ○ テストはメンテナンスしやすい状態であるべき ● データベースにCloud Spannerを利用 ○ 内製のORMを利用 ○ Truncateコマンドが存在しない ○ 外部キー制約を無効にできない

Slide 34

Slide 34 text

今回の事例における テストデータ生成時の注意点 ● 通常、テストデータを投入する際は外部キー制約 を無効にし、投入後有効にする ● Cloud Spannerでは外部キー制約を無効にして データ作成ができない ○ 親データから先にデータを作らなければならない

Slide 35

Slide 35 text

独自のテストデータ生成ツールを開発 ● 一般的なテストデータ管理ツールは Cloud Spannerでは利用できない ● FactoryBotのような使い勝手を目指して ○ データ生成時のインターフェースを似たように ● 内製のORMをベースにテスト用のORMを開発 ○ DAOのインスタンスを生成、永続化できるように ○ Build()でデータモデルを生成 ○ Create()でデータモデルを永続化

Slide 36

Slide 36 text

独自のテストデータ生成ツール ● 基本系:依存関係ないテストデータ ● 基本系:依存関係あるテストデータ ● 特殊系:特定状態を共有するためのテストデータ これらの3パターンの テストデータを生成するツールの紹介をします

Slide 37

Slide 37 text

独自ツール - 依存関係のないデータの生成 package model type User struct { UserId string `spanner:”UserId” span:“id”` Name string `spanner:”Name”` Email string `spanner:”Email”` } package factory type User struct { UserId *string Name *string Email *string } // フィールドに入力された値を元にモデルを生成 func (u *User) Build() *User // 永続化 func (u *User) Create(...) (*User, error) user := (&factory.User{ Email: String(“example.invalid”)}).Build() user.Create(ctx, dbinstance) テストデータ入力用 の構造体を準備 この構造体を使って テストデータを生成 入力フィールド ポインタ型にすることによって値が入力 されているかゼロ値か判定できる 入力されていない場合はランダム値、 もしくはテスト用のデフォルト値を利用 DAO (Data Access Object) テストに必要な フィールドだけ自 分で設定

Slide 38

Slide 38 text

独自ツール - 依存関係のあるデータの生成 package model type User struct { UserId string `spanner:”ID” span:“id”` Name string `spanner:”Name”` Email string `spanner:”Email”` RegionId string `spanner:"RegionId" span:”id”` } package factory type User struct { model *model.User Region *Region UserId *string Name *string Email *string } // u.Region.Create() を実行してから自身も永続化 func (u *User) Create(...) (*User, error) user := (&factory.User{ Email: String(“example.invalid”), Region: &factory.Region{Name: String(“Japan”)}}).Build() user.Create(ctx, dbinstance) テストデータ入力用 の構造体を準備 この構造体を使って データ生成し、永続化 依存テーブルのPK Regionテーブルの データ入力用の構造体 依存テーブルを含むデータが生 成される nilの場合は自動で作られるよう にすることもできる

Slide 39

Slide 39 text

独自ツール - 特定の状態の共有 user := (&factory.User{ Traits: []UserDatabuilderFunc{ factory.UserTraits.Banned(), factory.UserTraits.InvalidEmail(), }).Build() 状態を定義して生成

Slide 40

Slide 40 text

独自ツール - 特定の状態の共有 package factory type UserDatabuilder interface { Databuild(m *model.User) *model.User } type UserDatabuilderFunc func(m *model.User) *model.User func (f UserDatabuilderFunc) Databuild(m *model.User) *model.User { return f(model) } package factory var UserTraits = struct { Banned func() UserDatabuilderFunc InvalidEmail func() UserDatabuilderFunc }{ Banned: func() UserDatabuilderFunc { return func(m *model.User) *model.User { m.Banned = true return m } }, InvalidEmail func() UserDatabuilderFunc { return func(m *model.User) *model.User { m.Email = “example.com/invalid” return m } }, } package model type User struct { ... Banned bool Email string } テストデータの組み立てを インターフェース化 入力パラメータを元にデータを生成するロ ジックもこのインターフェースで実装される どういった状態なのか、 クロージャーを使って それぞれ定義していく

Slide 41

Slide 41 text

独自ツール - ここまで ● 基本的なデータ生成はできるようになった ● 今回の事例ではテーブルの数が200+ ○ 全部コードを書いていくのは困難 ● 自動でデータ生成のコードを生成したい

Slide 42

Slide 42 text

独自ツール - 自動生成機能 ● データアクセス用のモデルを静的解析して テストデータ生成のコードを自動生成 ○ Cloud SpannerのDDLを解析すると汎用性が高いツー ルにできるが、今回は開発スピード優先とした ● 自動生成したコードにメソッド単位でモデル毎の デフォルト値や生成ルールを手動で追加

Slide 43

Slide 43 text

独自ツール - 自動生成機能 ● 基本系:依存関係ないテストデータ ● 基本系:依存関係あるテストデータ ⇒ 自動化 ● 特殊系:特定状態を共有するためのテストデータ ⇒ 手動で作成

Slide 44

Slide 44 text

依存関係ないテーブルの判定 Table Users PK:UserId ⇒Table名とPKのカラム名が同じ ⇒依存関係なし

Slide 45

Slide 45 text

依存関係あるテーブルの判定 Table Users PK:UserId, RegionId ⇒Table名とPKのカラム名が違う ⇒依存関係あり 今回はSpannerのインターリーブ を想定しています

Slide 46

Slide 46 text

独自ツール - 自動生成機能 package model type User struct { UserId string `spanner:”ID” span:“id”` Name string `spanner:”Name”` Email string `spanner:”Email”` RegionId string `spanner:"AddressId" span:”id”` } package factory type User struct { Region *Region UserId *string Name *string Email *string } // 構造体に入力された値を元にモデルを生成 func (u *User) Build() *User // 構造体の永続化 func (u *User) Create(...) (*User, error) 依存関係あり データ入力用の構造体 とメソッドを自動生成 package factory type Region struct { RegionId *string Name *string } package model type Region struct { RegionId string `spanner:”ID” span:“id”` Name string `spanner:"Name"` } 依存関係ない

Slide 47

Slide 47 text

テストコード例 - ログインのテスト ● 必要な前提条件 ○ ログイン対象のUserデータがDBに存在すること ○ Passwordはハッシュ化してDBに保存されている ● テスト内容 ○ ユニークなEmailと、Passwordを使ってログイン ○ 登録されているリージョンからログインできること

Slide 48

Slide 48 text

テストコード例 - ログインのテスト func TestLogin(t *testing.T) { type inputs struct { email string password string } tests := []struct { name string testData *factory.User inputs inputs wantErr bool }{ { name: "正常系: 登録されているリージョンからログインできること", testData: (&factory.User{ Region: &factory.Region{ Name: "Japan"}, Email: "[email protected]", PasswordHash: generateHash("password")}).Build(), inputs: inputs{ email: "[email protected]", password: "password", region: "Japan"}, wantErr: false, }, } ● テスト名 ● DBに入れるデータ ● 検証対象の引数 ● 戻り値の検証

Slide 49

Slide 49 text

テストデータ生成ツールを作って良かったこと ● テストに必要なフィールドだけテスト毎にメンテ ナンスすれば良くなった ● 複雑なテストデータも管理しやすくなった

Slide 50

Slide 50 text

ここまでの流れ ● ビジネスロジックとDBが密結合していると自動テス トにコストがかかる ● テスト環境でDBの中身をまるごとテストデータにす れば密結合していても自動テストできる ● テストデータをコードで管理し、毎回生成すると自 動テストが見易い ● テストデータ生成を簡易化する生成ツールを開発

Slide 51

Slide 51 text

まとめ ● 自動テストが入ってないプロダクトを改善する大きな流れ ○ まず大きなテストを入れる ○ テストした箇所をリファクタリングしてよりテストしやすいものに する ● サーバアプリに自動テストを入れるための最初の一歩 ○ データベースを使っている場合はテストデータを管理できるように してテストを入れよう

Slide 52

Slide 52 text

No content