こんにちはみなさん
偉い人は言いました
Whenever you are tempted to type something into a print statement or a debugger expression, write it as a test instead.
何かを print 文やデバッガの式に書きたくなったときは、 代わりにその内容をテストに書くようにするんだ。
-- Martin Fowler
さて、これはPHPUnitのマニュアルサイトに、突如として書かれている文言です。
Martinさんといえば、アジャイルソフトウェア宣言をした偉人の一人です。
テストコードを書くとなると、なかなか敷居高いもので、特に既存システムがゴミだったりすると、導入など夢のまた夢ということもママあります。
一方で、echo
とかvar_dump
とか使って、こっそり内部動作を覗いて、コードがどこまで動いているか確かめたことのある人は多いのではないでしょうか。
Martin先輩の言うように、そういった確認コードをテストに置き換えてみてはどうでしょうか。
もしかしたら、テストを書くということが、割と簡単であるとわかるかもしれません。
今回はそんなお話です。
TL;DR
- ロジック中の
echo
のようなデバッグコードはテストに置き換えてみよう - テスト書くだけで仕様が確定するよ!
- テストってそんなに難しくないよ!
例題
ノーテストなコード
以下のコードを見てください
<?php
$nums = explode(',', $_GET['nums']);
$num1 = fibo($nums[0]);
$num2 = fibo($nums[1]);
echo $num2 * $num2 - $num1 * $num1;
function fibo($num)
{
if ($num == 0 or $num == 1) {
return 1;
}
return fibo($num - 2) + fibo($num - 1);
}
どこでこんなもの使うかはともかく、GET変数で取得した値をカンマ区切りで取得して、各々のFibonacci数列の該当する番号の値を取り出し、更に各々2乗した値の差を表示します。
これに対し、適当な値を使ってアクセスしてみます。
まず、ビルトインサーバを立ち上げて、
$ php -S 0.0.0.0:8888 app.php
次にcurl
でアクセスしてみます。
$ curl http://127.0.0.1:8888/?nums=1,3
8
$ curl http://127.0.0.1:8888/?nums=1,5
63
$ curl http://127.0.0.1:8888/?nums=5,7
377
さて、短いコードではありますが、途中で確かめたいことがあります。
Fibonacci数列になっているかを確認する
まず、この関数fibo
はどのような値を出力しているかを確認してみましょう。
app.php
の何処かに下のようなコードを書いてみます。
echo fibo(3);// 3
これはfibonacci数列の定義
a_{n+2} = a_{n+1} + a_n \ \ (a_0 = a_1 = 1)
に当てはめると、$a_3=3$に合っているのでこの結果は正しいです。
こんなコードを実コード中に入れてしまえば、値を出力することができます。
しかし、これはデバッグコードなので、最終的には削除しなければなりません。
しかも、ここで確かめた値が、この先ずっと同じであるということは、保証されません。
そこで外部にテストという形で、このデバッグコードの結果を残しておくことを考えてみましょう。テストするには何らかのテストスイートを入れてみるのがいいと思いますので、まずはphpunit
を突っ込んで、簡単なテストを作ってみましょう。
$ composer require --dev phpunit/phpunit
あ、composer
はグローバルインストールされていること前提ですぞ!
次に、テストしやすいように、関数fibo
を別クラスに持っていってみましょう。
<?php
require 'vendor/autoload.php';
$nums = explode(',', $_GET['nums']);
$fibo = new App\Fibo();
$num1 = $fibo->run($nums[0]);
$num2 = $fibo->run($nums[1]);
echo $num2 * $num2 - $num1 * $num1;
<?php
namespace App;
class Fibo
{
public function run($num)
{
if ($num == 0 or $num == 1) {
return 1;
}
return $this->run($num - 2) + $this->run($num - 1);
}
}
ここまで来ると、もうどうすればいいかは自明ですね。
<?php
use PHPUnit\Framework\TestCase;
class FiboTest extends TestCase
{
public function testRun()
{
$obj = new App\Fibo();
$this->assertEquals(3, $obj->run(3));
}
}
テストが書けました。
ユニットテストを走らせる場合は、適当な設定ファイルをおいておくと良いので、以下のファイルをルートディレクトリにおいておきましょう。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Application Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
</phpunit>
Laravelから一部パクっただけのやつです。
では、テストを通してみましょう。
$ vendor/bin/phpunit
PHPUnit 5.6.2 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 173 ms, Memory: 4.00MB
OK (1 test, 1 assertion)
テスト成功です。
さて、アサーションが一つだと心もとないので、もう少し増やしたくなりますね。
これはもう心置きなく、いくつでも増やしてあげると良いでしょう。
public function testRun()
{
$obj = new App\Fibo();
$this->assertEquals(3, $obj->run(3));
$this->assertEquals(5, $obj->run(4));
$this->assertEquals(8, $obj->run(5));
//以下略
}
さて、ここでこのテストとecho
による確認コードの違いを考えてみましょう。
echo
による確認は、いずれ削除しなければなりません。
しかし、テストコードにしてある場合は、わざわざ削除する必要はありません。
以降、テストを走らせるたびに、この確認が行われ、少なくともこのFibo
クラスの挙動に関し、問題ないことを常に確認していくことができます。
他のロジックもテストしてみる
関数fibo
をクラスFibo
のインスタンスメソッドに移すことで、テストができるようになりました。
この調子で、別のロジックの部分もクラスに追い出し、テストできるようにしちゃいましょう。
<?php
require 'vendor/autoload.php';
$action = new App\action\Diff();
echo $action->main($_GET['nums']);
<?php
namespace App\action;
use App\Fibo;
class Diff
{
public function main($get)
{
$nums = explode(',', $get);
$fibo = new Fibo();
$num1 = $fibo->run($nums[0]);
$num2 = $fibo->run($nums[1]);
return $num2 * $num2 - $num1 * $num1;
}
}
ここまでクラスに追い出してしまうと、テストも簡単です。
<?php
namespace action;
use PHPUnit\Framework\TestCase;
class FiboTest extends TestCase
{
public function testMain()
{
$obj = new \App\action\Diff();
$this->assertEquals(8, $obj->main('1,3'));
$this->assertEquals(63, $obj->main('1,5'));
$this->assertEquals(377, $obj->main('5,7'));
//以下略
}
}
ユニットテストを実行してみると。。。
$ ./vendor/bin/phpunit
PHPUnit 5.6.2 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 334 ms, Memory: 4.00MB
OK (2 tests, 6 assertions)
うまくいきました。
ロジックを変更してみる
さて、Fibo
のクラスのテストがすでにあるため、後でこのロジックを変更したいと思ったときも、テストが通っている限りはいくらでも変更できます。
これがリファクタリングです。
public function run($num)
{
if ($num == 0 or $num == 1) {
return 1;
}
$ret = 1;
$pre = 1;
for ($i = 1; $i < $num; $i++) {
$temp = $ret;
$ret = $ret + $pre;
$pre = $temp;
}
return $ret;
}
。。。いや、コードが汚くなってどうする。。。
それはそれとして、このコードをテストしてみます。
$ ./vendor/bin/phpunit
PHPUnit 5.6.2 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 334 ms, Memory: 4.00MB
OK (2 tests, 6 assertions)
なんとも安らかな気持ちでロジックの変更ができました。
テストコードがあるだけで、やりやすさがぜんぜん違うものです。
更にテストを拡張するなら
ここまでロジックをテストできる状況にしてしまえれば、以下のようなチェックとかができます。
- GETパラメータの有無のチェック
- パラメータが数値かどうかのチェック
長くなってきたので、これ以降は省略します。
デバッグコード書いたらテスト化しよう
デバッグコードをテスト化すると、とても良い利点があります。
1. テストが初められる!
テストの入っていないクソプロジェクト、クソコードは星の数ほどありますが、それらに包括的にテストを導入するのはとても辛く、しばしば不可能ですらあります。
しかし、今回やったように、echo
打つ代わりに対象ロジックをクラスに追い出してテストにする、というやり方であれば、割と誰でもできるのではないでしょうか。
2. 一度書いたら消さなくていい!
テストコードは一度書いたら消す必要がありません。
echo
やvar_dump
がプロダクトに混じっていたら完全に事故ですので、削除しなければならないのですが、テストコードはプロダクトには関係ないので。
で、自動テストを走らせればいつでも確認できるので、再度echo
とか書く必要がありません。
3. 仕様が決まる!
自動テストができるようになると、対象ロジックに対し内部ロジックをいくら変更したところで、テスト通っていればその挙動は同じであるとみなせます。
これはすなわち、そのロジックに対する仕様が決定したということを意味しています。
たとえ局所的に切り出しただけのテストであったとしても、コードで仕様を定義できたということはとても大きな一歩であるといえるでしょう。
4. リファクタリングができる!
テストコードで仕様が決定したロジックは、テストが通っている限りは、その内部ロジックおよびコードがどのようになっていようと問題ありません。
コードが気に入らなかったら、直してしまえばいいじゃない。。。そんなの、テストのない糞プロジェクトでは言えないことですが、テストさえ通っていれば楽勝です。
このように、仕様を変えない範囲でコードをきれいにしたり効率化したりすることを、リファクタリングといいます。
というか、自動テスト入ってないのに「リファクタリングする!」とか言っている方を見たことがあるのですが、どうやる気なんだろうかと不思議になりました。
5. ロジックを独立させられる
テストするにはクラスや関数に追い出すなど、なんとかしてロジックを切り出す必要がありますが、これは若干面倒くさいものの、ロジックを独立させることで再利用性を高めるという嬉しい副作用がついてきます。
まとめ
デバッグ用の確認コードをテストに落とし込むやり方を例示してみました。
会社の人に勧めておきながら、どうやってやるのってなるのもあれなのでって感じだったので。。。
テストを書く仕組みはかなり昔から存在しています。
PHPer界隈でもテスト書くことを多くの方が考え、実践し、発展してきています。
以前にも述べましたが、テストコードを書くことはエンジニアの義務だと考えています (現在はサーバサイド限定)。
無能な先人が作ったゴミコードに苦しんでいる方も多いかと思いますが、少しでもテストを導入して、品質を良くしていけると幸いです。
今回はこんな感じです。