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

DeNA TechCon 2021 - 自動テストのないプロダクトの開発効率化への道

DeNA TechCon 2021 - 自動テストのないプロダクトの開発効率化への道

DeNA TechCon 2021の発表資料です
https://techcon.dena.com/2021/session/14/

Junki Kaneko

March 03, 2021
Tweet

More Decks by Junki Kaneko

Other Decks in Technology

Transcript

  1. 自動テストの分類 - 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つに分類
  2. • 実装コスト • メンテナンスコスト Test Sizeのコストパフォーマンス Small Medium Large High

    Low テストピラミッド このような比重で 自動テストを実装すると コストパフォーマンスが良い
  3. リファクタリングの流れ - Pinning Tests 1. 外から見た動作を変えないように 大きめのテスト(Medium や Large Test)を入れる

    2. そのテストが担保している範囲でアプリケーショ ンの疎結合の度合いを上げるようなリファクタリ ングを実施 3. Small Testを入れてより粒度の高い確認をする 具体的には「レガシーコードからの脱却」を参照 https://www.oreilly.co.jp/books/9784873118864/
  4. 今回の事例 - 要件 • テーブル駆動テストでわかりやすくしたい ◦ テストデータをコードで定義 ◦ 特定の状態のデータは使いまわしたい (例:

    BANされたUser) • テストは使い捨てではなく今後も使い続ける予定 ◦ テストはメンテナンスしやすい状態にしたい • データベースにCloud Spannerを利用 ◦ 内製のORMを利用 ◦ Truncateコマンドが存在しない ◦ 外部キー制約を無効にできない
  5. データベースを利用したテストの流れ 1. データベースの Setup 2. テストデータの Setup 3. アプリケーションの Setup

    4. アプリケーションの実行 5. テストアサーション 6. アプリケーションの Teardown 7. テストデータの Teardown 8. データベースの Teardown テストデータとテストケース をどうやって管理するの か? テストデータをどうやって削 除するのか?
  6. テストデータの生成方法 • 静的な生成 ◦ SQLファイルで管理 ◦ データフォーマットを基に生成 ▪ github.com/go-testfixtures/testfixtures •

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

    動的な生成(ORMを利用前提) ◦ Goの構造体を基に生成 ▪ github.com/bxcodec/faker ▪ github.com/bluele/factory-go 今回はコードでテストデー タを管理したい
  8. [再掲]今回の事例 - 要件 • テーブル駆動テストでわかりやすくしたい ◦ テストデータをコード内で定義 ◦ 特定の状態のデータは使いまわしたい (例:

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

    BANされたUser) • テストは使い捨てではなく今後も使い続ける予定 ◦ テストはメンテナンスしやすい状態であるべき • データベースにCloud Spannerを利用 ◦ 内製のORMを利用 ◦ Truncateコマンドが存在しない ◦ 外部キー制約を無効にできない テストデータ、基本は共通化しないが 特定の状態を再利用できるようにする
  10. 今回の事例 - テストデータの管理方針 • テストデータの生成方法は? ◦ => Goのコード内 • どこまでテストデータの共通化をするのか?

    ◦ => 基本しない、どうしても共通化する場合は最小限 ◦ => 特定の状態のデータは共有したい • どうやってテストデータを削除するのか? ◦ => Transaction Rollback Teardown
  11. [再掲]今回の事例 - 要件 • テーブル駆動テストでわかりやすくしたい ◦ コード内で定義 ◦ 特定の状態のデータは使いまわしたい (例:

    BANされたUser) • テストは使い捨てではなく今後も使い続ける予定 ◦ テストはメンテナンスしやすい状態であるべき • データベースにCloud Spannerを利用 ◦ 内製のORMを利用 ◦ Truncateコマンドが存在しない ◦ 外部キー制約を無効にできない
  12. 独自のテストデータ生成ツールを開発 • 一般的なテストデータ管理ツールは Cloud Spannerでは利用できない • FactoryBotのような使い勝手を目指して ◦ データ生成時のインターフェースを似たように •

    内製のORMをベースにテスト用のORMを開発 ◦ DAOのインスタンスを生成、永続化できるように ◦ Build()でデータモデルを生成 ◦ Create()でデータモデルを永続化
  13. 独自ツール - 依存関係のないデータの生成 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) テストに必要な フィールドだけ自 分で設定
  14. 独自ツール - 依存関係のあるデータの生成 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の場合は自動で作られるよう にすることもできる
  15. 独自ツール - 特定の状態の共有 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 } テストデータの組み立てを インターフェース化 入力パラメータを元にデータを生成するロ ジックもこのインターフェースで実装される どういった状態なのか、 クロージャーを使って それぞれ定義していく
  16. 独自ツール - 自動生成機能 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"` } 依存関係ない
  17. テストコード例 - ログインのテスト • 必要な前提条件 ◦ ログイン対象のUserデータがDBに存在すること ◦ Passwordはハッシュ化してDBに保存されている •

    テスト内容 ◦ ユニークなEmailと、Passwordを使ってログイン ◦ 登録されているリージョンからログインできること
  18. テストコード例 - ログインのテスト 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に入れるデータ • 検証対象の引数 • 戻り値の検証