読者です 読者をやめる 読者になる 読者になる

CakePHPのテストコードを読んでみた

CodeIgniterにユニットテストをする環境はできたのですが、まだテストを書くのが手探り状態というのが正直なところです。


そこで、OSSなプロダクトでテストが付属しているものはないかなと探していたところ、CakePHPSimpleTestのコードが大量にあったので、早速読んでみる事にしました。


手始めにa()というCakePHP独自関数が、規模が小さく、本体、テストコード共に読みやすそうだったのでここから読み進めてみます。


なお、a()はcake/basics.php、a()のテストはcake/tests/cases/basics.test.phpにありました。


a()のコードは、

<?php
/**
 * Returns an array of all the given parameters.
 *
 * Example:
 *
 * `a('a', 'b')`
 *
 * Would return:
 *
 * `array('a', 'b')`
 *
 * @return array Array of given parameters
 * @link http://book.cakephp.org/view/694/a
 */
function a() {
	$args = func_get_args();
	return $args;
}

のようになっており、
ここで、func_get_args()は、php組み込みの関数で、
http://php.net/manual/ja/function.func-get-args.php に説明がありました。
可変長の引数を実現するためにつかう関数のようです。
a('a', 'b')のように呼び出すと、

<?php
array(	0 => 'a',
	1 => 'b'
)

のような配列で返ってきます。
このコードのテスト、testA()のテストは

<?php
/**
 * Test a()
 *
 * @access public
 * @return void
 */
function testA() {
	$result = a('this', 'that', 'bar');
	$this->assertEqual(array('this', 'that', 'bar'), $result);
}

のようになっていました。
a()に3つの引数を入れた時に、それらが配列で返ってきているかどうかテストしています。
続いて、aa()のコードをみてみます。
aa()は、aa('a', 'b')のように引数を渡すと、
連想配列array('a' => 'b')を返す関数です。

<?php
/**
 * Constructs associative array from pairs of arguments.
 *
 * Example:
 *
 * `aa('a','b')`
 *
 * Would return:
 *
 * `array('a'=>'b')`
 *
 * @return array Associative array
 * @link http://book.cakephp.org/view/695/aa
 */
function aa() {
	$args = func_get_args();
	$argc = count($args);
	for ($i = 0; $i < $argc; $i++) {
		if ($i + 1 < $argc) {
			$a[$args[$i]] = $args[$i + 1];
		} else {
			$a[$args[$i]] = null;
		}
		$i++;
	}
	return $a;
}

それに対応するのテストtestAa()は、

<?php
/**
 * Test aa()
 *
 * @access public
 * @return void
 */
function testAa() {
	$result = aa('a', 'b', 'c', 'd');
	$expected = array('a' => 'b', 'c' => 'd');
	$this->assertEqual($expected, $result);

	$result = aa('a', 'b', 'c', 'd', 'e');
	$expected = array('a' => 'b', 'c' => 'd', 'e' => null);
	$this->assertEqual($result, $expected);
}

のようになっています。
偶数個の引数と奇数個の引数を与えた時のパターンをテストしていますね。
1つめのパターンでは、

<?php
$args = array(
	0 => 'a',
	1 => 'b',
	2 => 'c',
	3 => 'd',
);
$argc = 4;

となるので、
1回目のループで、

<?php
$a[$args[0]] = $args[1]; // $a['a'] = 'b';

2回目のループで、

<?php
$a[$args[2]] = $args[3]; // $a['c'] = 'd';

となり、$aは、

<?php
array('a' => 'b', 'c' => 'd')

となりそうですね!

2つめのパターンでは、

<?php
$args = array(
	0 => 'a',
	1 => 'b',
	2 => 'c',
	3 => 'd',
	4 => 'e',
);
$argc = 5;

となるので、
2回目までのループまでは1つ目のパターンと同じです。
3回目のループで、elseの分岐に入るので、

<?php
$a[$args[4]] = null; // $a['e'] = null;

となり、$aは、

<?php
array('a' => 'b', 'c' => 'd', 'e' => null)

となりそうですね!
続いてam()を見てみます.
am()は配列をarray_merge関数を使って結合する関数です。

<?php
/**
 * Merge a group of arrays
 *
 * @param array First array
 * @param array Second array
 * @param array Third array
 * @param array Etc...
 * @return array All array parameters merged into one
 * @link http://book.cakephp.org/view/696/am
 */
function am() {
	$r = array();
	$args = func_get_args();
	foreach ($args as $a) {
		if (!is_array($a)) {
			$a = array($a);
		}
		$r = array_merge($r, $a);
	}
	return $r;
}

am()のテストtestAm()は以下です。

<?php
/**
 * Test am()
 *
 * @access public
 * @return void
 */
function testAm() {
	$result = am(array('one', 'two'), 2, 3, 4);
	$expected = array('one', 'two', 2, 3, 4);
	$this->assertEqual($result, $expected);

	$result = am(array('one' => array(2, 3), 'two' => array('foo')), array('one' => array(4, 5)));
	$expected = array('one' => array(4, 5),'two' => array('foo'));
	$this->assertEqual($result, $expected);
}

1つ目のパターンでは、引数に、1次元配列と数字を与え、全てが1次元配列で返ってくる事を確認しています。
2つ目のパターンでは、引数に、2次元の連想配列を与えてかつ、3番目の引数に与えている引数は1番目の引数に与えている引数とキーが同じです。テスト結果として期待するのは、後に与えた引数で上書きされるというものです。
array_merge関数の仕様(http://php.net/manual/ja/function.array-merge.php)を見てみると、
入力配列が同じキー文字列を有していた場合、そのキーに関する後に指定された値が、 前の値を上書きします
とあるので、正しそうです。
しかし、配列が同じ添字番号を有していても 値は追記されるため、このようなことは起きません。
とあるので、キーが数字だった場合は、添字があたらしく振り直されるようです。

この仕様を確かめるため、ちょっと本体にテストケースを追加してみます。

<?php
/**
 * Test am()
 *
 * @access public
 * @return void
 */
function testAm() {
	$result = am(array('one', 'two'), 2, 3, 4);
	$expected = array('one', 'two', 2, 3, 4);
	$this->assertEqual($result, $expected);

	$result = am(array('one' => array(2, 3), 'two' => array('foo')), array('one' => array(4, 5)));
	$expected = array('one' => array(4, 5),'two' => array('foo'));
	$this->assertEqual($result, $expected);
	// 追加したテストケース
	$result = am(array(0 => array(2, 3), 'one' => array('foo')), array(0 => array(4, 5)));
	$expected = array(0 => array(4, 5),'one' => array('foo'));
	$this->assertEqual($result, $expected);
	// 追加したテストケースここまで
}

追加したテストは、array_merge関数の仕様からすると、失敗するはずです。
まずは、テストは、テストがちゃんと行われているかを確かめるため、失敗させてから成功させるという手順で行うのが
定石だそうです。
この状態で、cakeのテストケースを実行すると、

のように、正しく(??)テストが失敗することが確認できます。
次に、テストケースを、

<?php
	// 追加したテストケース
	$result = am(array(0 => array(2, 3), 'one' => array('foo')), array(0 => array(4, 5)));
	// これを
	// $expected = array(0 => array(4, 5),'one' => array('foo'));
	// これに
	$expected = array(0 => array(2, 3),'one' => array('foo'), 1 => array(4, 5));
	$this->assertEqual($result, $expected);
}

のように書き直して実行してみます。これは成功するはずです。

成功していることが分かります。

普段は、チェックシートを作って、ここのロジックはこういうパターンがあるから、こういうデータを入れると、こういう出力になるはず...ということをしているのですが、この過程をテストコードに落とし込むという事が、頭だけでなく、体験として実感することができました。
しかも、テストコードを読むと、関数の仕様理解も深まっていい感じです!