Factory patternでデータベースのテストを効率化する

Atsushi Sumita
The Finatext Tech Blog
13 min readOct 13, 2020

--

An factory
An image of a factory from https://unsplash.com/photos/6xeDIZgoPaw

はじめに

こんにちは, FinatextグループのNowcastでデータエンジニア/データサイエンティストをやっている隅田(@yummydum)と申します.

データパイプラインを開発していると, データベースからデータを取り出し, 加工し, 結果を再度データベースに格納するという操作を(時には複雑なSQL等を通じて)行うことがよくあると思います. パイプラインの品質を高めるためにこれらの操作はしっかりテストされるべきです. テストの際にプロダクション環境のデータベースを使う訳にはいかないので, プロダクション環境に似せたテスト用のデータベースにテストデータを格納しておき, これをテストに用いれば良さそうです.

しかし, テストデータはうまく管理しないと保守性や可読性を保つのが難しくなります. そこで, 本記事ではテストデータの管理について以下の3つのパターンを具体的な実装を通じて紹介し, それぞれの特徴を理解していきます.

  • ベタ書き
  • Fixture Pattern
  • Factory Pattern

最終的には, factoryパターンがおすすめという結論になります. そこで, factoryパターンの実装に便利なFactory boyというライブラリをごく簡単に紹介します.

まずは前提を整理します.

言語とフレームワーク

言語はPythonでSQLAlchemyでデータベースとやりとりします.データエンジニアリングの文脈ではよくあるチョイスでしょう.またテストフレームワークはpytestを使用します.これらについては特に説明しませんが, ソースコードの要点は自然言語で説明するので, これらについて知らなくとも本記事で伝えたいことは伝わると思います.

テーブル定義

本記事の例で使用するテーブルの実装を示します.記事を読み進める上では, 次の三点が分かれば問題ないので, 気になる人以外はコードを読み飛ばして進んで下さい.

  • ユーザーのモデルであるUserテーブルが存在する
  • メールアドレスのモデルであるAddressテーブルが存在する
  • AddressUserの主キーを外部キーとして参照している

テストの対象

テストの対象としてget_addressesis_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と一対一で定義すれば良いです. 例えばUserFactoryUserのファクトリーメソッドの基となるクラスで, 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

--

--

Software engineer at Nowcast.Inc. My interest is in data engineering, data science, natural language processing, philosophy of language.