こんにちは。clonedです。
最近、改めてテストコードのあるプログラムは強い、と感じます。テストされている、変更による破壊を検知できる、というそのままの意味もありますし、テストが書ける程度にコードが整頓されている、つまり実装間違いが起きにくいコードであるという意味もあります。
テストを悪く言う人はあまりいないと思いますが、テストを書けない理由はいろいろあります。その中でも「テストを書いている時間がない」について考えます。
テストを書いている時間は本当にないのか
テストコードを書いて実行するよりも、ブラウザをリロードしたり、アプリを立ち上げ直した方が早く動作確認を行える場合、テストを書くことは “時間がかかること” になります。
大抵の場合、テストコードを書いて実行するよりも、アプリケーションを動作させる方が早いかもしれません(本当はそうでもないと思っていますが一般的な感覚値)。ですが、テストコードにはそもそもメリットがありますので、ほんの少し余分に時間がかかるだけなら(例えば、5分、10分程度)、テストコードを書いて実行する価値を見出しやすくなります。
テストを書いている時間がない、と思う場合、テストを書くと1時間、場合によっては数時間かかりそう、という見込みがあるのではないかと思います。
PHPUnitで時間かけずにテストを書くための工夫を紹介しようと思います。
PHPだけの空間を増やす
PHPの世界から一歩外に出ると途端にテストコードを書くことが難しくなります。つまりテストを書くのに時間がかかります。
データベースを使うテストはfixtureの準備が必要だったり、ファイルシステムを使うテストはMockを使うかテスト用のファイルを準備したりする必要があります。
PHPの世界だけのコードを実行するテストコードを書くのは非常に簡単です。
<?php
class Foo
{
public function hello(string $name): string
{
return 'Hello ' . $name;
}
}
<?php
use PHPUnit\Framework\TestCase;
class FooTest extends TestCase
{
public function testHello()
{
$foo = new Foo();
$this->assertEquals('Hello Japan', $foo->hello('Japan'));
}
}
これで、Fooクラスの hello()
メソッドを実行(テスト)できます。assertによるテストも重要ですが、実行できることも重要です。なぜなら、プログラマは作ったプログラムをまずは動かしてみたいからです。
次にデータベースを使う場合を考えます。
<?php
// Doctrineのような疑似コード
class Foo
{
private $entityManager;
public function __construct($entityManager)
{
$this->entityManager = $entityManager;
}
public function hello(int $id, string $name): string
{
$entity = $this
->entityManager
->getRepository('MyApp:Message')
->find($id)
;
return $entity->getMessage() . ' ' . $name;
}
}
取得したIDでデータベースから取得して、その値を利用するような処理です。こうなると一気にテストコードを書くのが難しくなります。
- 実際のデータベースを使う場合、messageテーブルにデータを事前に入れておく必要がある(fixtureの準備が必要)
- PHPUnitのMockを使う場合、
$entityManager
という複雑そうなオブジェクトを把握してMockを組み立てる必要がある
このようにPHPの世界を一歩でも出るとテストコードを書く気が一気に萎えます。
解決するには次のようなアプローチがあります。
- ここでデータベースから値を取得するのをやめる(引数で $message オブジェクトを受け取るようにする)。
function getMessage(int $id)
などをこのクラスに別途定義して単純なMockを組み立ててテストできるようにする
どちらも他所に押し付けているだけで根本的に解決していないように見えます。が、重要なことがあり、他所に押し付けたことで、 $entity->getMessage() . ' ' . $name
のテストが簡単に書けるようになるのです。
このメソッドではデータベースから値を取得している方が派手に見えますが、実際には文字列結合が意図した通りに動作しているかも重要なテスト事項です。
押し付けられた側では結局データベースを使ったテストが必要になるのですが、できるだけ最後まで押し付けていくと、データベースを直接使う処理がまとまっていきます。そこだけ諦めてデータベースを使ったテストを書くことになるかも知れませんが、他の部分で苦労せずに済みます。
PHPだけの空間を増やすことで、テストコードで実行しやすい場所が増え、コードカバレッジも上がります。
入力と出力だけでテストできるメソッドにする
最初の hello(string $name)
メソッドの例がそうですが、入力(引数)に対して挙動が確定し、出力(return値)を検証すれば良いコードほど簡単なテストはありません。
別の言い方をすると、インスタンス変数によって挙動が変わったり、戻り値以外で動作検証が必要だとテストを書く手間が増えます。プライベートなインスタンス変数はReflectionPropertyクラスなどを利用して操作できますが、テストコード内に準備のためのコードが多くなり見通しが悪くなります。何より萎えます。
1つのメソッドで複数のことをしない
Aを渡したらBを返す、のテストを書くのは簡単ですが、例えば、Aを渡したらAを元にループが始まり、ループの中で条件によって処理が異なるような場合、テストコードで網羅すべきパターンが増え複雑になります。
<?php
public function hello(array $params): array
{
$result = [];
foreach ($params as $param) {
if (isset($param['foo'])) {
$result[] = $param['a'];
} elseif (isset($param['bar'])) {
$result[] = $param['b'];
} else {
throw new \Exception('Unknown param');
}
}
return $result;
}
コードだけ見るとこれくらいは許してよという感じもしますが、テストコードを書く視点に立つと次のように見えます。
- ループするときとしないときがある
- ループ内に条件分岐がある
- ループ内の条件分岐によって戻り値が変化する
1つのメソッドで複合的なパターンを考えなければならないので次のようにメソッドを分けることで、それぞれのテストで考えることを減らすことができます。
<?php
public function hello(array $params): array
{
$result = [];
foreach ($params as $param) {
$result[] = $this->helloEach($param);
}
return $result;
}
private function helloEach($param)
{
if (isset($param['foo'])) {
return $param['a'];
} elseif (isset($param['bar'])) {
return $param['b'];
} else {
throw new \Exception('Unknown param');
}
}
一般的に、1つの複雑なメソッドのテストを書くよりも、2つの単純なメソッドのテストを書くほうが楽です。制御構造の中に制御構造、をできるだけ避けることでテストコードの複雑さはぐっと下がります。
テストしたいところだけをテストする(Mockをうまく利用する)
AクラスがBクラスを、BクラスがCクラスを利用しているような場合、Aクラスをテストしたいだけなのに、Cクラスが動くように準備しないとテストできない、という状況がよくあります。
このような場合にはMockが最適です。特にPHPUnitでは動的にMockが生成できるので、不要なMockクラスを作成する必要はありません。次はFooクラスがUserクラスに依存している場合の例です。
<?php
class Foo
{
public function hello(): string
{
return 'Hello ' . $this->getUserName();
}
protected function getUserName(): string
{
$user = new User();
return $user->getName();
}
}
class User
{
private $name;
// $this->nameに値を設定するような処理は割愛
public function getName(): string
{
return $this->name;
}
}
<?php
use PHPUnit\Framework\TestCase;
class FooTest extends TestCase
{
public function testHello()
{
// FooクラスのMockを生成
// コンストラクタは呼び出さない
// getUserName()をMock上で生成
$foo = $this->getMockBuilder(Foo::class)
->disableOriginalConstructor()
->setMethods(['getUserName'])
->getMock();
// getUserName()は1度だけ呼び出されて 'John' を返す
$foo->expects($this->once())
->method('getUserName')
->willReturn('John');
// Mockの挙動を使ってテスト
$this->assertEquals('Hello John', $foo->hello());
}
}
コード例が長くなるので単純化していますが、FooクラスのテストコードではUserのことを考えなくて済みます。useすら必要ありません。
もちろん、Userクラスが意図した動作をしない場合、このテストでは問題を検知できません。ですが、ユニットテストだからと割り切ってあまり気負いせずに書きやすくテストコードを書いた方が良いと思います。
データベースのところでは $entityManager
のMock化をデメリットとして書きましたが、フレームワーク/ライブラリのオブジェクトをMock化するにはそのクラスをよく把握する必要があり簡単とは言えません。ですが、今回のように自分がまさに開発しているコードであれば動作もよく把握しているのでMockの組み立てを簡単に行うことができます。
簡単とは言え、Mockを使わずに済むならその方がより簡単です。Web APIを使うような常にMockが必要な場合にPHPUnitの動的なMockを常に組み立てていると手間が多いので、指定したレスポンスを返すMockクラスを作った方が良い場合もあります。
終わりに
思いのほか長くなってしまったので、他の方法はまた機会があれば書こうと思います。
テストコードの簡単な書き方!ではなく、テストコードを簡単に書くためのアプリケーションコードの書き方ばかりになってしまいました(確信犯ですが)。テストコードのためにアプリケーションコードを変更するのは本質的ではない、という意見もあると思いますが、テストしやすいコードというのは、つまり呼び出し側にとって簡単に扱える優しいコードだと思いますので、結果問題ないと考えています。
昔、コードカバレッジ100%を謳う発表をしたことがありますが、目標は高く、というだけで、コードカバレッジのみを目的にする必要はありません。実行したいところをPHPUnitから簡単に実行できる、というスタート地点の克服が大切に思います。