自分がエンジニアになってから10年以上になり、10以上のプロジェクトで仕事をしてきましたが、どの現場でもネックになっていたことの1つがテストでした。
自動テストができれば一番それがいいのですが、自動テストをやること自体が簡単ではなく、すべてのケースで最適解というわけでもないです。
そこで自分がいろんなプロジェクトで経験してきた、テストにおける苦心だったりをいろいろとメモしておこうかなと思います。
私がPHP(Laravel)歴が長かったのでLaravelを前提にしている箇所もありますが、言語やFWはなんでもいいと思います。
1. 手動テストと自動テストの比較
それぞれの手法のメリデメや推奨アプローチなどですね。
1.1 手動テスト
メリット
- 実行だけに関していえばどんなテストでも手順書通りに行えば本番同様の再現が可能
- テスト項目書自体が仕様書になる
- 実行者のスキルに依存しない(開発の知識がなくても実行が可能)
- 実装者と別の人間が行うことで盲点に気づける(モンキー的なテストができる)
デメリット
- 作業が単調になりがちで、実施者のモチベーションをキープすることが難しい
- 規模が大きくなればなるほど、回帰テストのコストが膨らんでいく
- 項目書の作成(特に作成観点)はある程度のスキルが必要で、統一するのが難しい
- アジャイル的な修正が頻繁な場合には向かない(回帰的な再テストが必要でコストが膨れ上がる)
1.2 自動テスト
メリット
- 自動でテストが行えるため、システムの規模が大きいほど効果が大きい
- 特に回帰的なテストに強く、漏れに気づける
- n×m系の組み合わせが多い場合の確認などプログラムで簡単に確認できる
- アジャイル的な開発現場の場合、再テストの時間を短縮して、品質の担保が行える
デメリット
- 自動テストの実行にある程度の制約がある
- 自動テストをする環境構築自体がかなり手間がかかる
- テストコードを書く時間が単純にかかる(一般的にコード本体よりも長い時間がかかる)
- 自動テストで確認しにくい項目(UIの修正など)もあり、画面系のテストに弱い
- 大量のデータのテストなどに向かない
- システムが小規模な場合、構築コストを回収できない
- 実装者がテストコードを書くホワイトボックステストになりがちで、視点の抜け漏れが出やすい
- 外部サービスとの連携においてMock環境になるため環境依存の検証が難しい
- シナリオテストなどケースが複雑に入り組んだ部分のテストが難しい
1.3 推奨アプローチ
上記のようにコストやさまざまな事情があるため、現実的には以下のように手動テストと自動テストを組み合わせる必要が出てくると思います。
- 組み合わせが多いものなど、一般的な業務ロジックの大部分に関しては自動テストで担保
- 外部サービスとの連携や複雑なUIの挙動などに関しては手動テストで動きを担保
- 一連の業務フローの実行に関して大まかに手動テストを記述しておくことで、テスト項目書兼仕様書兼マニュアルとして機能させる
2. 自動テストの実装における障害と解決策
自動テストにはメリットも大きいのですが、自動テスト自体の実行がそもそも簡単ではないです。
ここでは私がぶち当たった技術的な課題と対処法などについて書いていこうと思います。
2.1 DBの初期化
問題:テスト用のDBでは、テストのたびにデータが変わってしまい、テストの担保ができない。(特に登録系)
解決策:関数ごとにテーブルデータ初期化(migrationを都度実行)を行う。
具体的実装:
- Laravelでは
use RefreshDatabase;コマンドでテストのたびにmigration実行が可能 - migrationが完備されており、テストのたびに新規のテーブルが作れる環境を整備
- 要はテストのたびにDBの状態を最新化して、0からその状態を始められることが必要
2.2 テストデータ作成
問題:テストデータの用意に時間がかかる。
解決策:
- テストのたびに前提となるテストデータがすぐに作れる仕組みを構築
- テストデータ作成用のメソッド(Laravelだとfactoryメソッド)を用意
- 業務系の複雑なパターンの場合、Excelから持ってくる方法もあるが、メンテなどのコストを考えると、コードで統一すべき
2.3 データ量の制限
問題:大量のデータがあると初期化に時間がかかる。(主にマスタ系)
解決策:
- マスタ系のデータは100レコード以下に制限
- 大量データが必要な場合は通常時はskipさせる処置を実装
2.4 外部連携
問題:API連携やAWS系のサービスとの結合が困難。
解決策:
- API連携:DIを使い、ローカル時にはJSONファイルなどで対応
- AWS系サービス:localstackなどテスト用の仮想環境を使用
- 代替手法:ラッピングしたメソッドを用意し、DIで代用
2.5 E2Eテスト
問題 APIのテストにくらべ、複雑なUIの総合的なテストは簡単ではない
解決策:
- モジュール単位での分割:なるべくモジュール単位でのテストで動作を担保する
2.6 テストコード導入の障壁(政治的課題)とその解決策
問題:プロジェクトでテストコードを書く手間を許容できるかという政治的課題。
解決策:
- 短期的には成果物の進捗にマイナスだが、長期的にはメンテナンスコストを大幅に削減
- プロジェクト開始時にテストコードの重要性について関係者の理解を得る
2.7 コード自体のテスト実行可能性
問題:そもそもコード自体がテストが実行可能な書かれ方をしているか。普段からテストが書けるコードになっていることが前提
解決策:
- 適切にモジュールに分かれているか(モジュールごとに分かれており、テストが実行しやすい)
- 短い関数を書くようにしているか(一般的に短い関数ほどテストしやすい)
- 責務の分離がしっかり行えているか(責務が大きすぎる場合、そもそもUnitテストの粒度を細かくすることができない)
3. テストコードの書き方における注意点
ここではテストコードを書く場合のポイントやコツについて書いておこうと思います。
3.1 タイトルをわかりやすく
仕様書としての目的があるため、関数名をなるべくわかりやすくすることが重要です。この際、日本語で書いても構いません。
「正常系1、異常系1」などではなく、何をテストしているのかがはっきりわかるようなテストが望ましいです。
3.2 値をなるべく具体的に書く
configやenvの値などを関数で値を持ってくるのではなく、直の値で直接書くようにしましょう。
理由:
- 設定が変わったことに気づかない
- 可読性を向上させることが目的
- テストコード=仕様書の役目が落ちてしまう
3.3 Mockやスタブの使用は最小限に
APIや外部環境などとの結合ではMockを使ったりすることもありますが、どうしてもMockを使わないと行けない場合以外は、なるべく本環境に近い環境でテストをするのが良いです。
内部のAPIなどをMock化してしまうと、APIの使用が変わったにも関わらず気づかずに通してしまい、つなぎ目の検証ができないためです。
3.4 仕様書として機能させる
そのため、単に動作正常性を担保するのではなく、ビジネスロジックをしっかりと担保しているか(前提データなどがそれに則っているか)を注視しましょう。
3.5 粒度の統一
例えばAPIのテストをする場合でも、大まかにざっくり分けると:
- エントリーポイントのFeatureテスト:APIの機能全体的なテスト
- Unitテスト:主にサブのモジュールの詳細なテスト
数としては必然的に Unitテスト > Featureテスト となります。
またカバレッジを使って見た目100%にしても粒度が整っていないと仕様書としての機能が弱くなるため、粒度の統一は必要です。
3.6 テスト同士の依存を生まないようにする
あるテストコードからあるテストコードを呼び出さないようにしましょう。
よくあるケースとしては初期データ投入などです。
この場合の対策:
- traitなどで独立したクラスを作る
- factoryパターンなどでデータを簡単に作れる仕組み作りを利用
- マスタ系に関してはseederとして独立させる
3.7 時間軸をずらす際の注意
時間が経ったことを確認したり、測定したりするテストでは、不用意に実装するとflakyテスト(実行結果が不安定なテスト)になります。
時間を経過させる場合、sleepを使ったりすることが多いと思いますが、不用意にこれを使うのではなく、時間を固定するような関数(LaravelでいうところのsetTestNow)を使うようにしましょう。
4. カバレッジの計測
自動テストがどれくらい現在のアプリのコードを網羅しているかを測定する指標としてはカバレッジが代表的かと思います。
PHPUnitなどでも出力できますが、codecovなどのサービスを使うとPRごとなどさらに細かい視点でコードの解析をしてくれます。
こちらも原理主義的になる必要はないですが、品質をはかる一つのバロメーターとしては機能すると思われます。
4.1 メリット
- 簡便でツールなどですぐに出すことができる
- 変更があった際にも強い
- テストが全く通っていない箇所の検知が楽で、偏りなどがすぐわかる
- カバレッジの推移などを見ることもできる
4.2 デメリット
- 一律な指標なので意味のない部分のテストが通っていなくても数字上の指標が出てしまう
- テストの質自体は問えない(単純にそこが通っているかいないかという以上の指標は出せない)
- 数値自体が一人歩きして、カバレッジを上げること自体が目的化してしまいがち
カバレッジは絶対視する必要はありませんが、ある程度の指標にはなり、弱い部分の発見などには有効です。