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. 自動テストのないプロダクトの
    開発効率化への道
    SWETグループ 金子淳貴

    View full-size slide

  2. 自己紹介

    氏名:金子淳貴(@theoden9014)

    所属:DeNA SWETグループ

    業務内容:

    Go言語プロダクトの開発効率化と品質向上

    ゲーム開発プロセス改善

    etc...

    View full-size slide

  3. この発表のゴール

    自動テストが入ってないプロダクトを改善する大きな流れ

    サーバアプリに自動テストを入れるための最初の一歩

    View full-size slide

  4. アジェンダ

    自動テストがないプロダクトの課題

    後から自動テストを導入するには?

    データベースを利用した大きめのテスト

    View full-size slide

  5. アジェンダ

    自動テストがないプロダクトの課題

    後から自動テストを導入するには?

    データベースを利用した大きめのテスト

    View full-size slide

  6. 自動テストが無いプロダクトの課題
    収益性や生産性が目標レベルを上回る確率は
    ハイパフォーマーはローパフォーマーの2倍
    LeanとDevOpsの科学 付録B 統計データより

    開発者が手元で動作確認

    一度開発したところが動かなくなる

    「手戻り」が発生しやすい、etc...
    効率が悪い
    自動テストを導入すると
    開発効率が上がる!

    View full-size slide

  7. 自動テストは簡単に導入できるのか?

    プロダクトの設計次第では、後から自動テストを
    入れるのに大きなコストがかかる

    以下のような設計だと自動テストを導入しにくい

    疎結合アーキテクチャになっておらず
    ロジックが動作する際に統合環境を必要とする

    例:ビジネスロジックとDB処理を切り離せない

    View full-size slide

  8. 自動テストの分類 - 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つに分類

    View full-size slide


  9. 実装コスト

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

    View full-size slide

  10. 理想は最初からSmall Testを書けるように
    ロジックを疎結合にしておく

    設計の段階でロジックを疎結合にしておく

    Layered Architecture

    Dependency Injection

    etc ....

    View full-size slide

  11. アジェンダ

    自動テストがないプロダクトの課題

    後から自動テストを導入するには?

    データベースを利用した大きめのテスト

    View full-size slide

  12. 後から自動テストを入れることができるのか?

    Small Testを入れるには設計、実装の段階で
    アプリケーションを疎結合にしておく必要がある

    そうでない時でも、リファクタリングすることで
    Small Testを入れられるようにすることはできる

    View full-size slide

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

    View full-size slide

  14. アジェンダ

    自動テストがないプロダクトの課題

    後から自動テストを導入するには?

    データベースを利用した大きめのテスト

    View full-size slide

  15. 大きめのテスト

    今回は一般的なWebシステムであれば必ずと言っ
    ていいほど利用されているデータベースを用いて
    いるシステムを対象

    具体的な事例を踏まえて紹介

    View full-size slide

  16. 今回の事例 - 概要

    自動テストが全く入っていない開発後期のプロダ
    クトに対して自動テストを導入

    Go言語で実装されているAPIサーバ

    DBと切り離してテストすることが難しい

    テーブルの数は200+

    View full-size slide

  17. 今回の事例 - 要件

    テーブル駆動テストでわかりやすくしたい

    テストデータをコードで定義

    特定の状態のデータは使いまわしたい
    (例: BANされたUser)

    テストは使い捨てではなく今後も使い続ける予定

    テストはメンテナンスしやすい状態にしたい

    データベースにCloud Spannerを利用

    内製のORMを利用

    Truncateコマンドが存在しない

    外部キー制約を無効にできない

    View full-size slide

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

    View full-size slide

  19. どうやって自動テストする?

    テスト環境用にDBを新たに構築

    DBの中身を全てテスト用データに置き換える

    ロジックを自動テストする

    DBの中身を空にする

    View full-size slide

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

    View full-size slide

  21. テストデータの管理で考えるべきこと

    テストデータの生成方法は?

    どこまでテストデータの共通化するのか?

    どうやってテストデータを削除するのか?

    View full-size slide

  22. テストデータの管理で考えるべきこと

    テストデータの生成方法は?

    どこまでテストデータの共通化するのか?

    どうやってテストデータを削除するのか?

    View full-size slide

  23. テストデータの生成方法

    静的な生成

    SQLファイルで管理

    データフォーマットを基に生成

    github.com/go-testfixtures/testfixtures

    動的な生成(ORMを利用前提)

    Goの構造体を基に生成

    github.com/bxcodec/faker

    github.com/bluele/factory-go

    View full-size slide

  24. テストデータの生成方法

    静的な生成

    SQLファイルで管理

    データフォーマットを基に生成

    github.com/go-testfixtures/testfixtures

    動的な生成(ORMを利用前提)

    Goの構造体を基に生成

    github.com/bxcodec/faker

    github.com/bluele/factory-go
    今回はコードでテストデー
    タを管理したい

    View full-size slide

  25. テストデータの管理で考えるべきこと

    テストデータの生成方法は?

    どこまでテストデータの共通化するのか?

    どうやってテストデータを削除するのか?

    View full-size slide

  26. どこまでテストデータを共通化するのか?
    テストデータを共通化すると
    テスト間で相互作用が発生する可能性がある

    例)テストデータを変えると関係のないテストまで落ちるよう  
    になってしまう
    共通化するとメンテナンスのしやすさは低下する⇒
    今回は基本共通化はしない
    詳細は”xUnit Test Patterns: Refactoring Test Code”
    今回はメンテナンスし易
    い状態にしたい

    View full-size slide

  27. [再掲]今回の事例 - 要件

    テーブル駆動テストでわかりやすくしたい

    テストデータをコード内で定義

    特定の状態のデータは使いまわしたい
    (例: BANされたUser)

    テストは使い捨てではなく今後も使い続ける予定

    テストはメンテナンスしやすい状態にしたい

    データベースにCloud Spannerを利用

    内製のORMを利用

    Truncateコマンドが存在しない

    外部キー制約を無効にできない

    View full-size slide

  28. [再掲]今回の事例 - 要件

    テーブル駆動テストでわかりやすくしたい

    テストデータをコード内で定義

    特定の状態のデータは使いまわしたい
    (例: BANされたUser)

    テストは使い捨てではなく今後も使い続ける予定

    テストはメンテナンスしやすい状態であるべき

    データベースにCloud Spannerを利用

    内製のORMを利用

    Truncateコマンドが存在しない

    外部キー制約を無効にできない
    テストデータ、基本は共通化しないが
    特定の状態を再利用できるようにする

    View full-size slide

  29. テストデータの管理で考えるべきこと

    テストデータの生成方法は?

    どこまでテストデータの共通化するのか?

    どうやってテストデータを削除するのか?

    View full-size slide

  30. どうやってテストデータを削除するのか?

    テスト毎にテーブルのデータを全て削除する

    Table Truncation Teardown

    Transactionを意図的に失敗させてロールバックする

    Transaction Rollback Teardown

    View full-size slide

  31. どうやってテストデータを削除するのか?

    テスト毎にテーブルのデータを全て削除する

    Table Truncation Teardown

    Transactionを意図的に失敗させてロールバックする

    Transaction Rollback Teardown
    Spannerには
    Truncateコマンドがない

    View full-size slide

  32. 今回の事例 - テストデータの管理方針

    テストデータの生成方法は?

    => Goのコード内

    どこまでテストデータの共通化をするのか?

    => 基本しない、どうしても共通化する場合は最小限

    => 特定の状態のデータは共有したい

    どうやってテストデータを削除するのか?

    => Transaction Rollback Teardown

    View full-size slide

  33. [再掲]今回の事例 - 要件

    テーブル駆動テストでわかりやすくしたい

    コード内で定義

    特定の状態のデータは使いまわしたい
    (例: BANされたUser)

    テストは使い捨てではなく今後も使い続ける予定

    テストはメンテナンスしやすい状態であるべき

    データベースにCloud Spannerを利用

    内製のORMを利用

    Truncateコマンドが存在しない

    外部キー制約を無効にできない

    View full-size slide

  34. 今回の事例における
    テストデータ生成時の注意点

    通常、テストデータを投入する際は外部キー制約
    を無効にし、投入後有効にする

    Cloud Spannerでは外部キー制約を無効にして
    データ作成ができない

    親データから先にデータを作らなければならない

    View full-size slide

  35. 独自のテストデータ生成ツールを開発

    一般的なテストデータ管理ツールは
    Cloud Spannerでは利用できない

    FactoryBotのような使い勝手を目指して

    データ生成時のインターフェースを似たように

    内製のORMをベースにテスト用のORMを開発

    DAOのインスタンスを生成、永続化できるように

    Build()でデータモデルを生成

    Create()でデータモデルを永続化

    View full-size slide

  36. 独自のテストデータ生成ツール

    基本系:依存関係ないテストデータ

    基本系:依存関係あるテストデータ

    特殊系:特定状態を共有するためのテストデータ
    これらの3パターンの
    テストデータを生成するツールの紹介をします

    View full-size slide

  37. 独自ツール - 依存関係のないデータの生成
    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)
    テストに必要な
    フィールドだけ自
    分で設定

    View full-size slide

  38. 独自ツール - 依存関係のあるデータの生成
    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の場合は自動で作られるよう
    にすることもできる

    View full-size slide

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

    View full-size slide

  40. 独自ツール - 特定の状態の共有
    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
    }
    テストデータの組み立てを
    インターフェース化
    入力パラメータを元にデータを生成するロ
    ジックもこのインターフェースで実装される
    どういった状態なのか、
    クロージャーを使って
    それぞれ定義していく

    View full-size slide

  41. 独自ツール - ここまで

    基本的なデータ生成はできるようになった

    今回の事例ではテーブルの数が200+

    全部コードを書いていくのは困難

    自動でデータ生成のコードを生成したい

    View full-size slide

  42. 独自ツール - 自動生成機能

    データアクセス用のモデルを静的解析して
    テストデータ生成のコードを自動生成

    Cloud SpannerのDDLを解析すると汎用性が高いツー
    ルにできるが、今回は開発スピード優先とした

    自動生成したコードにメソッド単位でモデル毎の
    デフォルト値や生成ルールを手動で追加

    View full-size slide

  43. 独自ツール - 自動生成機能

    基本系:依存関係ないテストデータ

    基本系:依存関係あるテストデータ
    ⇒ 自動化

    特殊系:特定状態を共有するためのテストデータ
    ⇒ 手動で作成

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  46. 独自ツール - 自動生成機能
    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"`
    }
    依存関係ない

    View full-size slide

  47. テストコード例 - ログインのテスト

    必要な前提条件

    ログイン対象のUserデータがDBに存在すること

    Passwordはハッシュ化してDBに保存されている

    テスト内容

    ユニークなEmailと、Passwordを使ってログイン

    登録されているリージョンからログインできること

    View full-size slide

  48. テストコード例 - ログインのテスト
    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に入れるデータ
    ● 検証対象の引数
    ● 戻り値の検証

    View full-size slide

  49. テストデータ生成ツールを作って良かったこと

    テストに必要なフィールドだけテスト毎にメンテ
    ナンスすれば良くなった

    複雑なテストデータも管理しやすくなった

    View full-size slide

  50. ここまでの流れ

    ビジネスロジックとDBが密結合していると自動テス
    トにコストがかかる

    テスト環境でDBの中身をまるごとテストデータにす
    れば密結合していても自動テストできる

    テストデータをコードで管理し、毎回生成すると自
    動テストが見易い

    テストデータ生成を簡易化する生成ツールを開発

    View full-size slide

  51. まとめ

    自動テストが入ってないプロダクトを改善する大きな流れ

    まず大きなテストを入れる

    テストした箇所をリファクタリングしてよりテストしやすいものに
    する

    サーバアプリに自動テストを入れるための最初の一歩

    データベースを使っている場合はテストデータを管理できるように
    してテストを入れよう

    View full-size slide