Laravel × Codeception × CircleCI でユニットテストを導入・自動化するまでの話

# はじめに

こんにちは！エン・ジャパン株式会社でバックエンドエンジニアをしております、武川です。
弊社のWebサービスであるengageは、2022年から開発内製化を進めております。
内製化に伴い、ユニットテストの導入とCIによる自動化を実施したので、その経緯を記します。

# 目的

- ユニットテストを導入し、シフトレフトで品質を担保できるようにする
- 開発者が継続してテストコードを書いていけるような、持続可能なテストの環境を整える

# 前提

- 言語：PHP
- FW：Laravel
- DB：SQL Server
  - DBスキーマはDDLファイルで管理、更新時はそのDDLを流す運用
    - マイグレーションは使用していない

# やったこと

## 1. 現状把握

- どうやら3年ほど前まではちょこちょこテストが書かれていた模様
    - 実行されず遺物と化したテストが100ファイルほど残っていたが、全て削除し0からのスタート
    - 実行環境は全くのゼロから構築というわけではなさそうだが、再整備は必要
- テスト用DBは、CREATE DATABASEしたものにDDLを流して作成していた
    - DDLは綺麗に管理されておらず、テストに必要なテーブルの洗い出しが必要
    - テスト用DBの構築方法も見直しが必要

ひとまず技術選定と、テスト用DB構築を含めた実行環境を整えることから始めました。

## 2. 技術選定

過去の選定を踏襲し、テスティングFWとして[Codeception](https://codeception.com/)を使用することにしました。
Codeceptionは一部独自の文法がありますが、機能や拡張性が豊富です。

- 単体テストだけでなく、機能テスト・受け入れテストもサポートしている
- PHPUnitを実行環境にしている
- 並列実行など、高速化の仕組みあり

## 3. 実行環境の整備

### テスト用DBをどうやって用意するか

#### それまで

- コンテナ立ち上げ時にCREATE DATABASEし、DDLを流して構築

#### 暫定対応

- ステージング環境のDBをダンプし、そのダンプファイルからリストアする
    - メリット
        - 導入はそれなりに楽（それなりに と書いたのは、SQL Serverは他のRDBMSに比べ、ダンプとリストアが複雑なため）
    - デメリット
        - スキーマ更新があるたびにダンプファイルを差し替える必要がある

#### 恒久対応

- マイグレーションを使用して構築
    - メリット
        - アプリケーション側でコード管理できる
        - テスト実行ごとにDBをfreshでき、独立性の担保に繋がる
    - デメリット
        - 初回導入時のみマイグレーションファイルの作成が大変

いきなり恒久対応を目指すのは時間がかかりそうだったので、まず暫定対応を挟むことにしました。

### 暫定対応：ダンプファイルのリストア

#### 問題発生

2022年6月当時、SQL ServerのDockerイメージがM1チップMacに非対応

#### 解決策

[Azure SQL Edge](https://hub.docker.com/_/microsoft-azure-sql-edge) + [sqlcmd](https://github.com/microsoft/go-sqlcmd)（go-sqlcmd）の構成に置き換え

- Azure SQL Edgeにはsqlcmdクライアントツールが同梱されているが、それもM1チップだと使えない...
→ 別途M1チップでも使えるsqlcmdコンテナを立ててDBに接続、そこからTransact-SQLを流してリストア実行することで解決

### 恒久対応：マイグレーションの使用

#### 必要なテーブルの洗い出し

- 削除されたテーブルや別DBに移行したテーブルのDDLが混在している状態だったため

#### マイグレーションファイルの作成

- 約150テーブル分のマイグレーションファイルが必要だった
- 何かスマートに作成できる方法はないか模索したが...
    - DBを元に自動でマイグレーションファイルを作成してくれるツール：[migrations-generator](https://github.com/oscarafdev/migrations-generator)
        - SQL Serverと相性△、手作業での調整が必須で結局手間になる
    - ダンプファイルをマイグレーションで実行してDB一括作成
        - SQL ServerとLaravelの相性△、うまくいかず

→ 結局、手作業で作成することに

## 4. 実装ポリシーの策定

#### ポリシーを詳細に定め、実装やレビュー時に迷いが生じないようにする

**ポリシーの一部抜粋**
- 1つのテストメソッドに条件分岐のテストを詰め込みすぎない、適宜分割する
    - dataProviderの活用
- DRYよりも愚直に
    - 過度な共通化よりも、上から読み下した時のわかりやすさを重視する
    - 参考：[リーダブルテストコード](https://speakerdeck.com/jnchito/number-vstat)
- テストメソッド名は日本語で、説明的な命名にする
    - 例：`test正常系_ステータスが1の時に、trueが返却され、レコードが保存されること()`

#### ポリシーと合わせてサンプルコードも実装

- テスト実装の経験が浅いメンバーのハードルを下げるため

## 5. CircleCIへの組み込み

- 作業ブランチをpushした際に発火するように設定
- 作業ブランチが最新でないままpushした結果、古い接続先を向いたまま`migrate:fresh`が走り、共用DBのデータを消し飛ばしてしまう事故発生
    - 正しい環境&正しい接続先でなければ`migrate:fresh`等の破壊的コマンドが実行されないよう制御を組み込んだ

## 6. その他

- 利用頻度の高いクラスや重要な処理をしているクラスから優先してテスト実装できるよう、優先度ランクを付けた
- テストコード専用のコードレビュワーを設定
    - メンバーにテスト実装のナレッジが浸透するまでレビューを継続する

## 7. テストコード運用開始

- 開発者全体に周知し、日頃の改修とセットでテストコードを書いてもらう運用を開始
- 継続して「テストコード書いてね」と呼びかけ
    - まずは書かないとテストのありがたみは実感できないため、心を鬼にして

# 課題

- ファットなクラスのテストを後から書くのはかなりヘビー
    - 実装ポリシーを充実させ、メソッドごとなどスモールステップで始めていく
- ランダムでテストが失敗することがある
    - fakerを使用してテストデータ生成している箇所で、DBの一意性制約に引っかかっていた
    - 見つけ次第こまめに潰し、ランダム値の扱いには注意するよう喚起
    - CircleCIの結果はグリーンに保っておかないと、あっという間に割れ窓状態になる

# おわりに

- 約1年の積み重ねにより、はじめ0だったテストファイルが480ファイルほどに増えた
    - とはいえカバレッジで見るとまだまだ数パーセント程度
- 新規プロジェクトの立ち上げ時からテストを書くのとは違い、既存の大きいサービスに後からテストを追加していくのはなかなか骨が折れる
- ただ、テストを書いたことで見つけられたバグも既に多数存在、テスト書いてて良かった〜
- 「テストコードを書きやすい実装」を意識するようにもなり、いい代謝が生まれている

今後も継続してテストを増やしていきながら、弊社の開発文化として定着・醸成していければと思います。