この記事はAll About Group(株式会社オールアバウト) Advent Calendar 2018 4日目の記事です。
みなさんは普段の開発者テストはどのようにやっているでしょうか? テストケースを書いて手動でテストをしている、テストコードを書いてテストをしている、はたまたやってないぜ!っていう硬派な感じでしょうか。私は前職からテストコードを書く習慣があるので開発者テストはテストコード+手動のテストという感じで行なっています。
そういう経験もあって周りから開発者テストについていろいろ聞かれることが多いので、今回のこの記事で自分がやっている中での習慣や、よく質問されることについて書いていきたいと思います。
導入編
テストコードを書く文化をインストールするタイミングを見計らう
「チームにテストコードを書いてもらえるようにしたいんだけど?」なんていう相談が来ることがあります。そんな相談にすぐさま飛びついて、意識を高く持っていきなりテストコードを書け!なんて言ってはいけません。テストコードを書きたいっていうチームであれば別ですが、だいたいの場合協力を得られることなく終わるでしょう。バグが多かったり、手動テストで思いの外期間がかかっていたりして課題感が出てきた時に、こういう方法ありますよ、っていう風にインストールすると意外とするっといきます。
初めてやるときは一緒にやる
さてテストコードを書いていこう!ということになって最初にやることといえば、そうペアプロですね。講義だけやってさあやってみてください、だと大体すぐつまづいて続かないので、プロジェクトに入ってペアプロで実際のコードを書いていくようにします。
最初は小さいステップを刻む
最初に始める時は小さいステップでやることが大事です。手順だけでなく、この状態だったらどういう結果になるか、意図しない結果になったらどういうことを伝えるには細かく体験してもらいます。私の場合は以下の順でやっていきます。
- テストコードが機能する(Errorにならずにfailする)ことを確認する
- テストコードから対象のクラス/メソッドを呼べることを確認する
- 失敗するテストを書く
- 失敗したテストが確実に通る一番シンプルな実装をする(仮実装してテストを固定化する)
- テストが通る最低限の実装して動くことを確認する(本実装)
- 以下、3-5を実施していく
- ある程度すすめたらリファクタリングする
1, 2は意外と重要で、ここでエラーになるときはtypoしていたり、設定が間違っていることがわかります。テスト自身が正しく機能することの確認はこのくらいしかないのでこのステップは必ずやってもらっています。
また、ステップごとにコミットをしてもらっています。こうすることであとからどういう手順で書いていったか、どういうことを意図してやっているのかを振り返ることができます。
実践編
常に最低限の実装を心がける
テストコードを書きながらやるときは一気に書かないようにします。一気に書いてしまうと何を検証したかったのかがわからなかったり、余計な実装をしてしまったり、リファクタリングのつもりでデグレさせたということになりかねません。常に最低限の実装を心がけて、シンプルなテストを書く → テストが確実に通る仮実装をする → 一番シンプル形で本実装する → リファクタリングの順番でやっていくと、いいリズムで素早くプログラミングできます。
テストメソッドは日本語(共用語)で書く
PHPUnitの話ではありますが、サンプルを見るとテストメソッドはtest
から始めることになっているので全部英語で書いたりしますが、だいたいの場合カッコつけてシンプルに書きすぎて結局このテストは何をしたいのかがわからなくなります。@test
アノテーションをつければテストメソッドの規則はなくなりますので共用語(まあだいたい日本語ですね)で誰が読んでもわかるように書きましょう。
public function testCheckRegistrated()
{
$user = User::create(['registered_at' => '2018-10-31 10:12:34', 'agreed_at' => '2018-10-31 10:13:45']);
$this->assertTrue($user->isRegistrated());
}
/** @test */
public function 登録したあとに規約に同意していたら本登録とみなす()
{
$user = User::create(['registered_at' => '2018-10-31 10:12:34', 'agreed_at' => '2018-10-31 10:13:45']);
$this->assertTrue($user->isRegistrated());
}
1つのテストには1つのassertionにする
よく1つのテストで複数のことをassertionしようとしますがそれは避けましょう。テストが失敗した時にどのassertionで失敗したのかわからなくなるからです。どうしても使う場合はメッセージを添えることをすすめていますが、あまりにも多くなる場合はおそらく対象のmethodが仕事をしすぎなので実装の方針変更(責務分割)も考えましょう。
フレームワークやライブラリの動きだけをテストしない
Webアプリの場合、だいたいはフレームワークを使っていると思いますが、テストコードをレビューしているとフレームワークの機構のテストだけが書かれていることが稀にあります。既存の機構のテストは意味がなのでやめましょう。
/** @test */
public function メールアドレスを登録できる()
{
Email::create(['email' => 'example@yourcompany.co.jp', 'user_id' => 1]); // Eloquentのテストになってる
$this->assertCount(1, Email::all());
}
新規で実装するときはどうやってテストコードを書くかから考える
新しいストーリーを実装する時にはいろいろ設計しながら進めると思いますが、どうやってテストコードを書くか、という観点をいれてみるとより良い実装(担保できるところが増える)で書くことができます。
例を挙げると、外部のAPIと連携したプログラムを書こうとした時にmethod内部で直接requestしてresponseをparseして処理するみたいに書くと思いますが、そんなプログラムにしてしまうとテストのときにAPIと疎通する必要がでてしまうのであまり良い実装ではなくなります。この場合はRequestするオブジェクトをDIにしてあげることでrequestまでの処理と、responseを受けとってからの処理がテストできるようになります。
class ApiMyService
{
public function findByName($name)
{
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', 'https://example.com/api/v1/items?name=' . $name);
if ($response->getStatusCode() !== 200) {
throw new \Exception('通信エラー');
}
$items = json_decode($response->getBody());
// 以下に$itemsをparseする処理
}
}
$service = new ApiMyService();
$service->findByName('なにかのアイテム名'); // 必ずAPIにrequestしてしまう
class ApiMyService
{
public function __construct($client)
{
$this->client = $client;
}
public function findByName($name)
{
$response = $this->client->request('GET', '/api/v1/items?name=' . $name);
if ($response->getStatusCode() !== 200) {
throw new \Exception('通信エラー');
}
$items = json_decode($response->getBody());
// 以下に$itemsをparseする処理
}
}
$service = new ApiMyService(new MockClient(['base_uri' => 'https://example.com']));
$service->findByName('なにかのアイテム名'); // APIには通信せずにrequest前後の処理をテストできる
ストーリーの受け入れ条件からテストケースを考える
テストコードを書くことを教えていると「どんなテストから書いたらいいですか?」と聞かれます。この質問が出て来るのはそもそも問題があるのですが、その辺を飲み込んであえて言うとストーリーの受け入れ条件をよく読んで、受け入れ条件をブレイクダウンしてそこからパターンをテストメソッドにしていきましょう。
改修するときは対象のテストを書くところから始める
プロジェクトをやっていくとテストコードが書かれていないクラスやメソッドを改修することはよくあることだと思います。たとえ1行追加するだけ、条件を反対にするだけ、みたいな処理だとしても今動いているところのテストを書いてから改修するようにしましょう。変更する前の状態をテストが担保してくれるようになるだけで安心して改修することができるようになります。
最初から完璧にやろうとしない
テストコードを書くことをルールにしていくと、完璧にやることが目的になることが稀にあります。とても大事なのですが窮屈になって続かなくなってしまいます。別にテストコードだけで全てを終わらせるわけではないし、なにより開発の目的はプロダクトをユーザーに素早く届けることですので、ここはテストコードを書く、ここは実際に動かしてやるなど判断しながらやりましょう。実際に動かしてみてテストコードで表現できそうだな、と思ってからテストコードにしても遅くはないので、力量・期限などを見極めてやっていきましょう。
テストコードだけを書いて終わらせない
テストコードが上手にかけてくるようになるとバグも少ない状態で、動くものが素早くできるようになります。テストコードを書いてあるから大丈夫!と思ってそこで終わったつもりになってしまう人がいますがそれではいけません。テストコードが正しく動いているからといってストーリーの受け入れ条件を満たしているわけではありません。テストコードはあくまで実装が意図したとおりに動作することを担保できるのであって、受け入れ条件を勘違いしていることまでは担保していません。きちんとユーザーと同じように操作して確認しましょう。
ボーイスカウトルールを適用する
きのこ本にも書かれている有名な話ですが、コードを改修したときは改修する前よりも美しくというルールでやりましょう。極力既存のコードを触らないようにという考え方でやっている人がいますが、テストコードを書いたり、ドキュメントを直したり、typoを直したり、規約違反をなおしたり、、、実際の改修には関係ない箇所でもメソッド内をちょっと見直すと綺麗にできることはあったりします。そういうのを見つけたら積極的に直していきましょう。ただしやる場合は変更前の動きをテストコードで担保してからおこなうように。間違ってもテストがないところまでやってはいけない。
テストコードもリファクタリングする
テストコードを書いていくと準備する処理が重複してきたり、フラグを変えてデータのパターンを切り替えたりしますが、あとからみた時やレビューするときに冗長でとてもわかりにくいです。テストコードもプロダクトコードと同様にリファクタリングをしてきましょう。データの生成はfactory methodを作って適切な名前をつけてあげるととてもスッキリしてよいコードになります。
$admin_user1 = User::create(['role' => 'admin']);
$admin_user2 = User::create(['role' => 'admin']);
$member_user1 = User::create(['role' => 'member_']);
$member_user2 = User::create(['role' => 'member_']);
$member_user3 = User::create(['role' => 'member_']);
$admin_users = factory(User::class, 2, 'admin_user')->create();
$member_users = factory(User::class, 3, 'member_user')->create();
テストの実行結果を定期的にフィードバックする
テストコードを書いたら定期的に実行してチームに通知しましょう。テストがこけたらその日にマージした何かが原因ということがすぐわかりますし、単純にテストが順調に増えていくのが通知されると書いている実感が小さなモチベーションにアップに繋がります。最近ですとCIツールで回すことが多いと思いますが、環境を用意してcronで回すでも問題はありません。(実際に今のチームはcronで実行して結果をslackに投げてます) テストが失敗していることに気づいたらすぐ修正しましょう。
さいごに
テストコードを書けるというのは開発者にとってとても強い武器になると思います。テストコードもプロダクトコードと同じように綺麗に書いて、気持ちのよい開発ライフを送りましょう。