はじめに
2017年は、Symfony2
のプロジェクトにひたすら機能テストを書いていました。
そこで得た知見をまとめていきたいと思います。
尚、ここでいう機能テストは、KernelTestCase
を継承したテストを意図しています。
KernelTestCase
を継承したテストは、Kernel
を通じて様々なことを自動でしてくれるので大変便利です。
- サービスコンテナにアクセスできる(依存オブジェクトの注入などを自動でやってくれる)
-
EventDispatcher
を通じて、イベントを発火してくれる(単体テストではできない)
Symfony2における機能テスト、KernelTestCase
については下記のリンクを参照ください。
どういった環境か
アプリケーション
- Symfony 2.8
- 一つのWebアプリはAPIと複数のバンドルを利用して構成される
- Webアプリ側には、ほとんどソースがない
- コントローラも機能ごとにバンドルに束ねられている(例:会員系のバンドル、決済系のバンドル)
- テンプレートは、Webアプリ側(
app/Resources/XxxxBundle
)にある -
Domain Kataの
Usecase
に主なビジネスロジックは実装
データベース、Doctrine ORM
周り
-
postgresql
8.3/8.4 withpgpool
- データベースの制約少なめ(ユニーク制約、外部キー制約など)
- データベースが先にあって、
Symfony2
を後から導入した格好 -
Doctrine ORM
のリレーションはほぼ未使用 - マイグレーションなし
- 社内共用サーバーのデータベースにアクセスして開発するスタイル
テスト周り
- 単体テストは少しあるが、あまりメンテはされていなさそう
-
Scrutinizer CI
でビルドが回っているが、静的解析のみで自動テストはなし
やってきたこと
2017年1月〜3月
- 小さめの機能追加やリファクタリングをやりながら、徐々に全体を把握していく
- Codeceptionを使った受入テストを実装
2017年4月〜6月
- 毎日決まった時間に
pg_dump
を使って、開発DBのDDLと最低限のマスターデータをSQLファイルに出力し、S3にアップロードするようにした - DBを使った機能テストを
Scrutinizer CI
で実行するようになった- ビルド時に、S3からダウンロードした最新のDDL、マスターデータをリストア
2017年7月〜9月
- 今までテストがなかったバンドルに、テストを実装していく
- 主要なバンドルにはテストがある状態になった
2017年10月〜11月
- バンドル側に実装が偏っていたので、徐々にアプリ側に実装を移していく
- アプリ側に実装を移していくのにあわせて、アプリ側のテストも実装
得られた知見
マイグレーションがなければ、正となるDBからリストアする方法もある
PostgreSQL
ならpg_dump
、MySQL
ならmysqldump
といったダンプツールがあるので、正となるDBを定期的にバックアップして、ローカルやCI環境にリストアする方法があります。
尚、CIなどイミュータブルな環境であれば、データベース作成 => リストアで良いのですが、既存のDBに差分更新をかけたい場合は、別のツールが必要になります。
PostgreSQL
ならapgdiff
というJAVA
製のツールがあるので、差分更新用のSQLを生成することができます。
単体テストでも受入テストでもなく、機能テストを重視すべき
冒頭でも触れましたが、単体テストではイベントが発火されないので、イベントが発火した後の状態を検査できません。
また、モックオブジェクトが増えれば増えるほど、テストコードが増える割にカバレッジはそれほど増えません。
一方、受入テストは、カバレッジは増やしやすいものの、テスト対象が広範囲なので、一つ一つのテストが大きく、実装の難易度も高いのが難点です。
Symfony
はDI(Dependency Injection)
の仕組みが備わっており、機能テストしやすい構造を持っていますので、機能テストに注力するのが良いと思います。
バンドル単体の機能テスト
バンドル単体の機能テストについては、Symfonyと言えばおなじみの「カルテットコミュニケーションズ」さんの技術ブログにいい記事があります。
ブロク記事の内容そのままですが、ポイントは下記のとおりです。
- テスト用の
Kernel
を作る - テスト用の
Kernel
にバンドルを登録する - テスト用の設定をテスト用の
Kernel
にロードさせる - テスト用の
Kernel
で機能テストする
バンドル側にあるコントローラであっても、テストはアプリ側でやるべき
同じバンドルであっても、それを利用するアプリによって、設定やテンプレートが変わる(カスタマイズ可能)ので、バンドルのソースだったとしても、コントローラに関してはアプリ側でやりましょうということです。
ログインユーザーをシミュレートする
任意のユーザーでログインしたことにする方法が、Symfony
の公式ドキュメントにて紹介されています。
これによって、ユーザーのロールによって異なる挙動などがテストできます。
doctrine-test-bundle
はマスト
コントローラのテストを動かしていると、下記の事象に出くわすことがありました。
-
setUp
でbeginTransaction
して、テストデータを登録し、tearDown
でrollback
-
persist
=>flush
しているにも関わらず、データを取得するとnull
になる
テストデータの登録時と参照時でコネクションが別になってしまっていることが原因として考えられます。
そこで、doctrine-test-bundle
を利用すると、コネクションが一元管理されるので、上記のような事象が解決されます。
詳細については、下記のリンクからREADME
を参照してください。
テスト実行時だけ、任意のサービスを差し替えることができる
config_test.yml
を経由して、テスト時のサービス設定を下記のようにします。
# app/config/config_test.yml
imports:
- { resource: services_test.yml }
# app/config/services_test.yml
services:
service.foo:
class: AppBundle\Tests\Fake\FakeFooService
するとテスト時には、既存のservice.foo
サービスがFakeFooService
に差し替えられます。
ただ、フェイクを多用すると、テストされないコードが増えるだけなので、どうしてもテストできない部分(外部システムのAPIコールなど)に限定するようにしましょう。
発見したバグ
テストを書いている最中に、ひょっこり発見したバグがいくつかありました。
投げられない例外
例外が投げられることを確認するテストを書いて実行したら、テストがコケたことで気付きました。
静的解析やIDEを使えば気付けるのかもしれませんが、最終的にはテストで未然に防ぎたいものですね。
if (/* 何か例外的な状況で */) {
new Exception('ま、throwしないけどな');
}
// 処理続行しちゃうよ...
尚、Java用の静的解析ツールであるfindbugs
には検出項目があるのですが、Scrutinizer-CI
にもphpmd
にもそういった検出項目はなさそうです。
例外が投げられていることを確認するテストを書きましょう。
通らないコード
PHP
は宣言なしに、変数を使うことができるので、下記のようなコードであっても例外にはなりません。
が、その条件が真になることはないので、通らないコードとなります。
private function doSomething($items)
{
if (!empty($item)) { // $items の間違い
// 結果、このブロックを通ることはない
}
さすがに、Scrutinizer-CI
はBug
としてこれを指摘していました。
ただ、他の指摘に埋もれてしまったのか、僕が気付くまでそのままになっていました。
結果的に、その条件で何かすることは仕様的に不要と判断されたので、if文ごとコードを削除したのですが、何も考えずにバグを直してしまっていたら、それはそれでバグ扱いされていたかもしれません。
通らないコードは後になって時限爆弾のようにエンジニアを苦しめるだけなので、作り込まないようにテストを通しておきましょう。
おわりに
フルコミットではなかったので、約1年かかってしまいましたが、テストを書く文化、土壌みたいなものは作れたと思います。
- 安全にコードを変更していくためには、テストが必要
- テストを通じて現状のソースコードを知ることができる
- まずは自分が安心するために、テストを書く
来年もSymfony
に限らず、PHP
に限らず、テストを書きまくりたいと思います。
ではでは。