この記事は DMM Advent Calendar 2017 その2 の記事です。
前日は @sasakipochi さんの中国クラウドで金盾体験の話でした。
まえがき
テストがないと死ぬ病気にかかってしまった@wkubota@githubです。
会社ではコードレビューおじさんと化していますが、心はプログラマーです。
私は弊社にはいって2年ちょっと、直近の1年半は既存システムのリプレイスに関わって来ました。
その期間にUnit Testによる自動化が無い状態からあるのが普通になるまでシステムとしても組織(部署)としても成長できたのですがふと立ち返ると前職でも同じような経験をしており、Unit Testを文化として成長させていく段階で比較的多くの人が経験することかなと思いこれから同じようなことをする人たちの転ばぬ先の杖となるようなまとめを書いていこうと思います。
※弊社が2年前Unit Testが無かったというわけではありません。自分が携わったリプレイス対象の既存システムとその関係チームがUnit Testに不慣れであったというだけの一部の話であることをご承知おきください。
つまりこういうことです↓
記事の目的
- Unit Testを成長させるために必要な転ばぬ先の杖
記事の対象
- Unit Testがあまり無い状態から充足した状態へ組織を成長させたい人
前提条件
本記事で、Unit Test
という用語を多用しますが、xUnitによるテストおよびテストケースの意味で使っています。
Unit Testを行うことが決まっているという条件で話します。(それ以前の導入すべきか否かという判断に関わる部分は割愛)
技術的な要件
開発言語:PHP
testing framework:PHPUnit
以下、Unit Test文化の成長段階に合わせて起こる問題と対処方法について順を追って見ていきましょう。
成長段階
初めて各機能にUnit Test を書いていく段階。テストコードを書くコストが比較的多くなり敬遠する人も多い状態。
Unit Test 書かない問題
ほんとこれ。
これに尽きるといっても過言ではないです。
ぶっちゃけ、この後の問題全部あわせてもこの問題よりは大変じゃないです。
で、この問題への対処法は Unit Test書かないとレビュー通さない(リリースしない)
ってなるんですが
そこまで杓子定規にやっていても何もすすまないので原因を聞いてそれぞれ対処法をみんなで考えるっていうスタンスが良いと思っています。
Unit Test書いたこと無いから書けない場合
対処方法
- お手本を提示する。(うまく合う既存テストがあればそれを、無ければ大枠くらいは作ってあげる)
時間がないから書けない場合
対処方法
- 最低限必要(大事な所とかあやしい所とか)な所とそうじゃないところを切り分けて、後回しにする文は別タスクとして切り出す。
後回しのタスクもちゃんとチケット化して管理対象にいれるのがポイント。
難しくて書けない場合
元のコードがテストしづらいっていうのはあり得ること
その場合どうするか?
対処方法
- テストしやすいように元のコードのリファクタリングをオヌヌメする。(クラス設計こうした方が良いとかそのくらいまでは一緒に考える。)
- (外部API叩く等の外部要因てきな難しさ) mockの使い方を工夫する
拡充段階
Unit Testを機能とセットで書くことが軌道に乗り始めた段階。根幹部分にはテストコードが揃ってきたが、テストがない機能も多い状態。
一つのテストケースで複数のケースの検証しちゃう問題
ちゃっちゃとテスト書いて確認したいとやってしまいがち(なのか?)のこの問題
見つけ方は1テストメソッドで複数の条件を何度もテスト対象メソッドを呼び出して検証してたりするとこの問題です。
例:
/** @test */
public function handle()
{
// 準備等省略
// 検証その1
$result = $target->handle($parm1);
$this->assertNull($result);
// 検証その2
$result = $target->handle($parm2);
$this->assertEquals($expected, $result);
}
対処方法
- 一つのテストケースで確認したいパターンは一つにする。
- パラメタライズドテストを導入することで防げる場合も多い
類例
* パラメタライズドテスト知らない問題
テストケース内で例外を基底クラスでcatchしてしまう問題
例外が発生するパターンのテストでテストメソッドをtry catchで囲んで\Exception
のような基底クラスでキャッチしてしまうという問題です。
/** @test */
public function handle()
{
// 準備等省略
try {
$result = $target->handle($parm1);
$this->fail($result);
} catch (\Exception $e) {
// 諸々検証したり…
}
}
この場合、該当処理がバグってて例外でてもcatchして意図した動きと判定してしまう可能性があり良くないです。
対処方法
- PHPUnitの場合
@expectedException
アノテーションを使う(どのxUnitにもこの手のものがだいたいあります。)
類例
* @expectedException系のアノテーションを知らない問題
他にもこのような問題が発生したりします。
(ちょっと長くなりすぎてしまうので以下省略)
- mock使いすぎ問題
- プライベートメソッドのテストが横行する問題
- テストクラスでtest対象以外のメソッドも
test
プリフィクスつけてしまってテストケースとして認識されてしまうのに気づかない問題 - テストの基底クラスで色々やってしまう(DI)のでテストが遅くなる問題
- テスト対象外のクラスをテストクラスで使ってしまう問題
- リフレクションの使いどころ問題
- 使っても良いけど程々に
- 標準出力(var_dump)で確認して満足しちゃう問題
- メソッドチェーンのテストしづらい問題
- 実装だけのプルリクが来てテストのプルリクは後で問題
成熟期
Unit TestとCIによる自動化がほぼ揃うようになった段階。Unit Testが無い機能はあまり無い状態。
この頃になるとテストケースは1リポジトリ数千とかなってきたりします。(リポジトリの粒度どうなん?ってのはあるけど・・・。)
テスト流さない問題
テストスイートが大きくなってくると自分の触った箇所のテストケースしか実行しないというこの問題が発生します。
対処方法
- CIの側でリポジトリの全テストケースを回すので個々人が全ケースを気にする必要が無い状態にする。
個々人に頑張れと言っても限度があります。
テストスイートの粒度を細かくして流しやすくするというのも進める必要がありますが一番手っ取り早いのがCIで流すから気にすんなっていう方向ですね。
類例
* 他のテストが失敗しても気にしない問題
* 簡単な修正だからとテスト直さない問題
まとめ
以上、Unit Testを成長させていくなかで出会った諸々の事象のまとめでした。
もうちょっと深掘りしたほうが良さそうなのですがそれは別の機会に。
おかげさまで、僕がテストがないと死ぬ病気
を発症させて死ぬ確率はだんだん下がってきています。
テストを書いているすべての開発者に感謝ですヽ(´エ`)ノ
明日は@norihさんが書いてくれます!