Symfony2のプロジェクトにひたすら機能テストを書いた話

## はじめに
2017年は、`Symfony2`のプロジェクトにひたすら機能テストを書いていました。
そこで得た知見をまとめていきたいと思います。

尚、ここでいう機能テストは、`KernelTestCase`を継承したテストを意図しています。
`KernelTestCase`を継承したテストは、`Kernel`を通じて様々なことを自動でしてくれるので大変便利です。

- サービスコンテナにアクセスできる（依存オブジェクトの注入などを自動でやってくれる）
- `EventDispatcher`を通じて、イベントを発火してくれる（単体テストではできない）

Symfony2における機能テスト、`KernelTestCase`については下記のリンクを参照ください。

- [Your First Functional Test - Testing (Symfony 2.8 Docs)](https://symfony.com/doc/2.8/testing.html#functional-tests)
- [KernelTestCase.php](https://github.com/symfony/framework-bundle/blob/2.8/Test/KernelTestCase.php)

## どういった環境か
### アプリケーション
- Symfony 2.8
- 一つのWebアプリはAPIと複数のバンドルを利用して構成される
- Webアプリ側には、ほとんどソースがない
- コントローラも機能ごとにバンドルに束ねられている（例：会員系のバンドル、決済系のバンドル）
- テンプレートは、Webアプリ側（`app/Resources/XxxxBundle`）にある
- [Domain Kata](https://github.com/phpmentors-jp/domain-kata)の`Usecase`に主なビジネスロジックは実装

### データベース、`Doctrine ORM`周り
- `postgresql` 8.3/8.4 with `pgpool`
- データベースの制約少なめ（ユニーク制約、外部キー制約など）
- データベースが先にあって、`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を生成することができます。

- [Another PostgreSQL Diff Tool (apgdiff)](https://www.apgdiff.com/)

### 単体テストでも受入テストでもなく、機能テストを重視すべき
冒頭でも触れましたが、単体テストではイベントが発火されないので、イベントが発火した後の状態を検査できません。
また、モックオブジェクトが増えれば増えるほど、テストコードが増える割にカバレッジはそれほど増えません。
一方、受入テストは、カバレッジは増やしやすいものの、テスト対象が広範囲なので、一つ一つのテストが大きく、実装の難易度も高いのが難点です。
`Symfony`は`DI（Dependency Injection）`の仕組みが備わっており、機能テストしやすい構造を持っていますので、機能テストに注力するのが良いと思います。

### バンドル単体の機能テスト
バンドル単体の機能テストについては、Symfonyと言えばおなじみの「カルテットコミュニケーションズ」さんの技術ブログにいい記事があります。

- [Symfonyバンドル単体のファンクショナルテスト | QUARTETCOM TECH BLOG](http://tech.quartetcom.co.jp/2016/12/19/functional-testing-syfony-bundle/)

ブロク記事の内容そのままですが、ポイントは下記のとおりです。

- テスト用の`Kernel`を作る
- テスト用の`Kernel`にバンドルを登録する
- テスト用の設定をテスト用の`Kernel`にロードさせる
- テスト用の`Kernel`で機能テストする

### バンドル側にあるコントローラであっても、テストはアプリ側でやるべき
同じバンドルであっても、それを利用するアプリによって、設定やテンプレートが変わる（カスタマイズ可能）ので、バンドルのソースだったとしても、コントローラに関してはアプリ側でやりましょうということです。

### ログインユーザーをシミュレートする
任意のユーザーでログインしたことにする方法が、`Symfony`の公式ドキュメントにて紹介されています。

- [How to Simulate HTTP Authentication in a Functional Test (Symfony 2.8 Docs)](https://symfony.com/doc/2.8/testing/http_authentication.html#creating-the-authentication-token)

これによって、ユーザーのロールによって異なる挙動などがテストできます。

### `doctrine-test-bundle`はマスト
コントローラのテストを動かしていると、下記の事象に出くわすことがありました。

- `setUp`で`beginTransaction`して、テストデータを登録し、`tearDown`で`rollback`
- `persist` => `flush`しているにも関わらず、データを取得すると`null`になる

テストデータの登録時と参照時でコネクションが別になってしまっていることが原因として考えられます。
そこで、`doctrine-test-bundle`を利用すると、コネクションが一元管理されるので、上記のような事象が解決されます。
詳細については、下記のリンクから`README`を参照してください。

- [doctrine-test-bundle](https://github.com/dmaicher/doctrine-test-bundle)

### テスト実行時だけ、任意のサービスを差し替えることができる
`config_test.yml`を経由して、テスト時のサービス設定を下記のようにします。

```yml
# app/config/config_test.yml

imports:
    - { resource: services_test.yml }
```

```yml
# app/config/services_test.yml

services:
    service.foo:
        class: AppBundle\Tests\Fake\FakeFooService
```

するとテスト時には、既存の`service.foo`サービスが`FakeFooService`に差し替えられます。

ただ、フェイクを多用すると、テストされないコードが増えるだけなので、どうしてもテストできない部分（外部システムのAPIコールなど）に限定するようにしましょう。

## 発見したバグ
テストを書いている最中に、ひょっこり発見したバグがいくつかありました。

### 投げられない例外
例外が投げられることを確認するテストを書いて実行したら、テストがコケたことで気付きました。
静的解析やIDEを使えば気付けるのかもしれませんが、最終的にはテストで未然に防ぎたいものですね。

```php
if (/* 何か例外的な状況で */) {
    new Exception('ま、throwしないけどな');
}

// 処理続行しちゃうよ...
```

尚、Java用の静的解析ツールである`findbugs`には検出項目があるのですが、`Scrutinizer-CI`にも`phpmd`にもそういった検出項目はなさそうです。

- [作成した例外をスローするのではなく捨てている - FindBugs Bug Descriptions](http://findbugs.sourceforge.net/bugDescriptions_ja.html#RV_EXCEPTION_NOT_THROWN)

例外が投げられていることを確認するテストを書きましょう。

### 通らないコード
`PHP`は宣言なしに、変数を使うことができるので、下記のようなコードであっても例外にはなりません。
が、その条件が真になることはないので、通らないコードとなります。

```php
private function doSomething($items)
{
    if (!empty($item)) { // $items の間違い
         // 結果、このブロックを通ることはない
    }
```

さすがに、`Scrutinizer-CI`は`Bug`としてこれを指摘していました。
ただ、他の指摘に埋もれてしまったのか、僕が気付くまでそのままになっていました。

結果的に、その条件で何かすることは仕様的に不要と判断されたので、if文ごとコードを削除したのですが、何も考えずにバグを直してしまっていたら、それはそれでバグ扱いされていたかもしれません。
通らないコードは後になって時限爆弾のようにエンジニアを苦しめるだけなので、作り込まないようにテストを通しておきましょう。

## おわりに
フルコミットではなかったので、約１年かかってしまいましたが、テストを書く文化、土壌みたいなものは作れたと思います。

- 安全にコードを変更していくためには、テストが必要
- テストを通じて現状のソースコードを知ることができる
- まずは自分が安心するために、テストを書く

来年も`Symfony`に限らず、`PHP`に限らず、テストを書きまくりたいと思います。

ではでは。