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

CakePHP2でFixtureのデータをCSVから任意のタイミングで変更する

CakePHPのFixtureは個人的に使い辛いと常々感じていて、理由としては

  • 1Fixtureクラスに対して1データしか用意できない
  • 状況によりデータを変えたい場合1Modelに対して複数のFixtureクラスを用意する必要がある

といったことなどから基本CakePHPではできるだけFixtureを使わずモックで済ませてきました。

ですが列数が多いDBのデータをモックで返すことや、DBも参照してテストしたい場面があったので、CSVからFixtureのデータを読み込むのをさくっと作ってみました。

この方法以外にも、PHPUnit_Extensions_Database_DataSet_CsvDataSet使うとか Fabricate 使うとか状況によって色々あるかと覆いますが参考までにコード載せときます。

app/Test/AppTestCase.php

<?php
App::uses('FileUtil', 'Utility');
App::uses('CakeTestFixture', 'TestSuite/Fixture');

class AppTestCase extends CakeTestCase
{

	/**
	 * CSVファイルを読込んでファイル内容を返します。<br>
	 * CSVファイルは実行するテストクラスと同じディレクトリに配置します。<br>
	 * ※1.CSVファイルのファイルサイズは1MBまで<br>
	 * ※2.CSVファイルの文字コーディングはShift_JIS<br>
	 * ※3.ヘッダー行には列名が必要<br>
	 *
	 * @param string ファイル名
	 * @return array 配列の配列を返します
	 */
	protected function readCsv($filename)
	{
		$class = new ReflectionClass($this);
		$testPath = pathinfo($class->getFileName());
		$csv = $testPath['dirname'] . DS . $filename;

		if (!file_exists($csv)) {
			throw new CakeException("Could not find CSV file. [$csv]");
		}

		$size = filesize($csv);
		if ($size > 1048576) {
			// 最大1MBまで
			throw new CakeException("CSV File is over maximum size. [$csv]-[$size]");
		}

		$data = array();
		$fp = fopen($csv, "r");
		// ヘッダー行取得
		$header = fgetcsv($fp);
		while ($row = fgetcsv($fp)) {
			mb_convert_variables('UTF-8', 'SJIS-win', $row);
			$array = array();
			foreach ($header as $index => $column) {
				if (array_key_exists($index, $row)) {
					$array[$column] = $row[$index] != 'NULL' ? $row[$index] : null;
				} else {
					$array[$column] = null;
				}
			}
			$data[] = $array;
		}
		return $data;
	}

	protected function loadCsvFixture($fixtureName, $filename)
	{
		$fixture = substr($fixtureName, strlen('app.'));
		$fixturePath = TESTS . 'Fixture';

		$className = Inflector::camelize($fixture);
		if (is_readable($fixturePath . DS . $className . 'Fixture.php')) {
			$fixtureFile = $fixturePath . DS . $className . 'Fixture.php';
			require_once $fixtureFile;
			$fixtureClass = $className . 'Fixture';
		} else {
			throw new CakeException('Could not read fixture class. ' . $fixtureName);
		}

		$fixture = new $fixtureClass();
		$data = $this->readCsv($filename);
		$fixture->records = $data;

		/** @var $fixtureManager AppFixtureManager */
		require_once TESTS . 'AppFixtureManager.php';
		$fixtureManager = new AppFixtureManager();
		$fixtureManager->loadSingle($fixture);
	}

	/**
	 * プライベートメソッドを実行します。
	 *
	 * @param object $obj インスタンス
	 * @param string $methodName メソッド名
	 * @return mixed 返り値
	 * @throws CakeException
	 */
	protected function invoke($obj, $methodName)
	{
		if (!isset($obj) || !is_object($obj)) {
			throw new CakeException('The parameter obj is not an object.');
		}
		$clazz = new ReflectionClass($obj);
		$method = $clazz->getMethod($methodName);
		$method->setAccessible(true);

		$countArgs = func_num_args() - 2;
		if ($countArgs > 0) {
			$array = array();
			$args = func_get_args();
			for ($i = 0; $i < $countArgs; $i++) {
				$index = $i + 2;
				$array[] = $args[$index];
			}
			$result = $method->invokeArgs($obj, $array);
		} else {
			$result = $method->invoke($obj);
		}
		return $result;
	}
} 


app/Test/AppFixtureManager.php

<?php
App::uses('CakeFixtureManager', 'TestSuite/Fixture');
App::uses('CakeTestFixture', 'TestSuite/Fixture');

class AppFixtureManager extends CakeFixtureManager
{
	/**
	 * @see CakeFixtureManager::loadSingle
	 */
	public function loadSingle(CakeTestFixture $fixture, $db = null, $dropTables = true)
	{
		if(isset($fixture)) {
			if (!$db) {
				$db = ConnectionManager::getDataSource($fixture->useDbConfig);
			}
			$this->_setupTable($fixture, $db, $dropTables);
			$fixture->truncate($db);
			$fixture->insert($db);
		} else {
			throw new UnexpectedValueException(__d('cake_dev', 'Referenced fixture class is not found'));
		}
	}
} 


使い方

  • AppTestCase.phpを継承したクラスを作成

class XxxTest extends AppTestCase {}

  • テストメソッド内で呼び出す

$this->loadCsvFixture('app.Xxx', Xxx.csv');

この場合、XxxTest.phpと同じディレクトリにXxx.csvを配置します。

Xxx.csvのファイル内容の1行目には列名、2行目以降にデータを入力しておきます。

loadCsvFixture実行時にCSVからデータを読込み、DBの値を更新します。