Factory patternでデータベースのテストを効率化する
はじめに
こんにちは, FinatextグループのNowcastでデータエンジニア/データサイエンティストをやっている隅田(@yummydum)と申します.
データパイプラインを開発していると, データベースからデータを取り出し, 加工し, 結果を再度データベースに格納するという操作を(時には複雑なSQL等を通じて)行うことがよくあると思います. パイプラインの品質を高めるためにこれらの操作はしっかりテストされるべきです. テストの際にプロダクション環境のデータベースを使う訳にはいかないので, プロダクション環境に似せたテスト用のデータベースにテストデータを格納しておき, これをテストに用いれば良さそうです.
しかし, テストデータはうまく管理しないと保守性や可読性を保つのが難しくなります. そこで, 本記事ではテストデータの管理について以下の3つのパターンを具体的な実装を通じて紹介し, それぞれの特徴を理解していきます.
- ベタ書き
- Fixture Pattern
- Factory Pattern
最終的には, factoryパターンがおすすめという結論になります. そこで, factoryパターンの実装に便利なFactory boyというライブラリをごく簡単に紹介します.
まずは前提を整理します.
言語とフレームワーク
言語はPythonでSQLAlchemyでデータベースとやりとりします.データエンジニアリングの文脈ではよくあるチョイスでしょう.またテストフレームワークはpytestを使用します.これらについては特に説明しませんが, ソースコードの要点は自然言語で説明するので, これらについて知らなくとも本記事で伝えたいことは伝わると思います.
テーブル定義
本記事の例で使用するテーブルの実装を示します.記事を読み進める上では, 次の三点が分かれば問題ないので, 気になる人以外はコードを読み飛ばして進んで下さい.
- ユーザーのモデルである
User
テーブルが存在する - メールアドレスのモデルである
Address
テーブルが存在する Address
はUser
の主キーを外部キーとして参照している
テストの対象
テストの対象としてget_addresses
とis_hogemails
という関数を想定します.話を簡単にするためのtoy exampleなので, この関数何に使うんや??といった疑問は一旦置いておいて下さい. 実際の例としては複雑なSQLを投げている関数などをイメージして頂ければと思います. 関数の中身については以下の点だけ理解すれば記事を読み進めるには十分です.
get_addresses
については,
- 機能:データベースから
Address
のレコードを全て取得する - テストデータの要件: 正常な
Address
のレコードが複数入っていること
is_hogemails
については,
- 機能: 全ての
Address
レコードが正しいhogemailアドレスであるかを判定する - テストデータの要件: 不正なhogemailアドレスを保持している
Address
が一つ以上入っていること
テストの種類
テストといっても様々な種類が存在します. Unit test, integration test, functional test, end2end testなど様々な用語が(しばしば異なる意味で)用いられていますが, ナウキャストではgoogleが提唱しているTestSizeの基準に則って開発を行っています.
この記事ではMedium testを前提に考えていきます. データベースに関連するところで具体的に言うと, モックはせずにテスト用のデータベースと実際にやりとりするということです.
3つのパターン
前置きが長くなりましたが,以下で具体的に3つのパターンを見ていきましょう.なお, コードは全てこちらで公開しています.
ベタ書き
それぞれのテスト毎に必要なデータを愚直にsetupするパターン(?)です.Address
のレコードをデータベースに格納する際に外部キー制約を満たすためにUser
レコードを用意する必要がある点に注意してください.辛そうな雰囲気だけ伝われば良いので流し読みでおkです.
見て分かる通り,このパターンはコードが非常に 冗長で 可読性が低い です.要因としては外部キー制約を満たすためにUser
レコードを用意する必要があることや,二つのテストケースにおいて重複が相当程度生じていることが原因として挙げられます.
例えばテーブル定義に変更が生じてUser
の初期化の仕方を変えなければならない場合,これらのテストケースは全て手作業で修正する必要が生じます(実際やった経験アリ).また,一見すると二つのテストケースで同じテストデータを使用しているように読めますが,実は細部で差があります(探してみて下さい).どこに差があるかひと目では分からないので,毎回何がテストされているのかを頑張って目視しなければなりません(はい,これも経験アリなのです).
とはいえ, いかなる場合でもこのパターンを避けるべきという訳ではありません. 例えば一回限りのセットアップで重複が存在しないとか外部キー制約がないとかコンストラクタの引数が少ない等の理由から実装が簡潔に済む場合などはこのパターンがリーズナブルになるかもしれません. 問題は冗長性や可読性の低さなので, それが生じていない限りは良い訳です.
Fixtureパターン
上記のコードを見ると, テスト間で共有するデータを一つの箇所にまとめてしまえば冗長さが減って嬉しいと考えるのは自然でしょう.この共有されるデータがfixtureと呼ばれるものです(pytest.fixture
とは違う概念なので注意). 以下のtest_data()
がそれに該当します.
重複が排除されたため,もしsetupの実装に変更が生じてもfixtureを変更するだけで良くなりました.また,全てのテストケースが同じ条件でテストされていることも保証されています.
しかしそれとは裏腹に,重複をなくしたことで 柔軟性が失われてしまいました.is_hogemails
のテストデータとしては不正なメールアドレスが入力されたレコードが欲しいのですが,上記のfixtureではこの要件が満たされていません.この変更のせいでtest_is_hogemails
が通らなくなりました. かといって全てのテストで不正なメールアドレスが入力されたレコードを使用する訳にもいきません. このような微妙にテストケース毎に異なる要件を満たすのが難しくなりました.
また,今後様々なテーブルやテストケースを追加するにつれ,fixtureはどんどん肥大化してゆきます. 結果として,可読性の低さが問題となります. その結果, それぞれのテストケースがfixtureにどう依存しているかが不明瞭になり, fixtureを変更することがリスキーになります.
Factoryパターン
重複はなくしたいが柔軟性も保ちたい.その上可読性も良くしたい.そこで活躍するのがfactoryパターンです.このパターンの精神は, テストに関係ないデータは隠蔽するということです. 例えば, Address
テーブルに正常なレコードが3つ入ってさえいれば十分なテストデータについて, どのような引数でUser
を初期化しデータベースに格納するかという操作は重要ではないので, 裏側でよしなにやってくれれば良い訳です. 逆にテストに関係しているデータは明示的にしておきます. 例えば不正なemail_address
が必要ならば, 不正なemail_address
がどのようなデータなのかが一目で分かるように記述します.
Factoryパターンを実装するためには, まずAddress
クラスのファクトリーメソッドAddressFactory
を実装します.これはattributeを良い感じに自動で埋めたAddress
オブジェクトを返す関数です. 引数でattributeをオーバーライド出来るようにし, 柔軟性を保ちます. ファクトリーメソッドの具体的な実装については次節で紹介することにして,まずはテストケースでインターフェースを確認しましょう.
まずtest_address
においてはcreate_batch()
を呼び出すだけでattributeがよしなに埋められた3つのAddress
オブジェクトがデータベースに格納されています. テストの前提条件を満たすためには, これで十分です.
次に, test_is_hogemails
を見てみると, ファクトリーメソッドの引数においてemail_address
をオーバーライドしています. このテストでは不正なemail_address
が入力されていれば他のattributeは何でも良いので, デフォルト値で埋めてしまえばOKです.
これらの例から分かるように, factory patternは重複を排除しつつも柔軟にテストデータを生成することが出来ます. また, 何がテストされているかが非常に明確であることが分かります. というのも, テストに関係のないattributeは自動生成されるので隠蔽されており, テストに関係のあるattributeは明示的に指定されているからです.
Factory boy
上記の例はfactory boyで実装されています. これはデータベースのテストにおいてfactory patternを実装するためのライブラリです. 上記の例を見て, "外部キー制約を満たすためのUser
の初期化はどうなっているのか?"と思った方もいるかもしれませんが, 実はfactory boyの機能で必要な外部キー制約を満たすように自動でUser
レコードを生成し永続化してくれています. このように便利なfactory boyなのですが, 検索してもあまり情報が出てこないし, 慣れない内は複雑なので, この記事をきっかけに入門して頂ければと思います.
Factoryクラスの定義
まずはどのようなテストデータを生成したいかをクラスで定義します.基本的にはmodelsと一対一で定義すれば良いです. 例えばUserFactory
はUser
のファクトリーメソッドの基となるクラスで, user_id
などのUser
クラスのattributeをどのように埋めるべきかが宣言的に記述されています.
このコードについて最低限の解説を行います.
Sequence
factory boyにはFaker
もサポートされており, ランダムに氏名やメールアドレスを埋めることが可能なのですが, 個人的にはSequence
を使用する方がおすすめです. というのも, ランダムに生成すると各attributeがユニークなのか重複しているのかが分からなくなるからです. ユニークなものを重複させるのは簡単ですが逆は難しいので, 基本的にはユニークをデフォルトにした方が良いと思います.ユニークにするためにはSequenceを使う方法が公式でも推奨されています.
SubFactory
別のクラスのオブジェクトがattributeとなる場合に, そのクラスのファクトリーメソッドを呼び出してattributeにセットしてくれます. SQLAlchemyのモデルにrelationship()
によりuser
を指定しておかないと対応するattributeがありませんと怒られるので注意.
SelfAttribute
自身の他のattributeから動的に別のattributeを生成するためのメソッドです. この例では単にuser_id
を自らのattributeにセットしています.
Smallテストをどう実装するか?
ここまでmediumテストを前提に話して来ましたが, smallテストでも同じコードを使いまわしたいはずです. factory boyはデータベースに接続せずにモックオブジェクトを生成することにも使えます.
ポイントは以下の3つです.
- factoryクラスが参照している
SESSION
にengineをバインドしない - small testにはマーカーを付与しておき, small test以外のテストをスキップ出来るようにする
- small testの内部では
Factory.build()
だけを使用する
これでデータベースと接続せずにsmall testだけを実行出来ます. pytestにおいてはconftest
でこれらの設定を行えば良いです. conftestについては公開しているレポジトリのここ, small testの具体例についてはここを御覧ください.
他のfactory patternの実装
非常に便利なfactory boyなのですが, 学習コストが高く導入しづらいというデメリットがあるのも確かです. そこでもう少しお手軽で導入しやすい別のfactory patternの実装についても軽く触れておきます.
まず, pytestのドキュメントではfixtureを使ってfactoryを実装するfactory as fixtureパターンが紹介されています. 次に, SQLAlchemyのmodelsにテストデータを生成する関数を実装するパターンです. 例えばUser
クラスにcreate_test_data()
などの関数を実装して, 自らのattributeを適切に埋めて引数でオーバーライド出来るようにしておきます. これについては詳しくは こちらの動画で議論されているのでぜひご覧下さい.
終わりに
本記事ではfactory patternを用いることで柔軟性/可読性/保守性の高いテストを実現出来ることを紹介しました. 今回のテーマ以外にもパイプラインのテストには様々なポイントがあるので, 是非次節の参考資料をきっかけに情報収集して見て下さい.
また, 私の所属するFinatextグループでは様々なエンジニアを募集しております. 是非以下の採用ページを御覧ください!
参考資料
テストの定義や原則について
https://testing.googleblog.com/2010/12/test-sizes.html
https://testing.googleblog.com/2018/02/testing-on-toilet-cleanly-create-test.html
https://testing.googleblog.com/2019/12/testing-on-toilet-tests-too-dry-make.html
データベースのテストについて
https://www.youtube.com/watch?v=a713rcagoYU
https://www.youtube.com/watch?v=ZBLaHL1mTW0
Factory boyについて
https://github.com/FactoryBoy/factory_boy
https://github.com/FactoryBoy/factory_boy/blob/92cc94d50ec892a0bada258be661dd544b45219d/docs/introduction.rst