Yii Unittest with MockUp

I use the Yii framework for one month and I find information of Unittest for the Yii framework at many places. But I don’t find an complete example of an unittest with datafiles in memory and mockup for Yii classes. So I write down my solution.

I want to read a datafile from my “FritzBox” that is in ascii format into my MySql database. For the import I need to read the file line by line and generate SQL-Insertstatements. In this example I don’t use ORM only DAO. The import function work correct but I want a unittest for the function. The unittest should only test the import and not the Yii functionality for DAO and I don’t want an real database management for the test. So I want to use mockups for the class CDbConnection and CDbCommand. The testdata should by in spezial fixture files (PHP-File) and not originaly datafiles from my “FritzBox”. I use for that ‘vfsStream’ for the test datafile.

My CFormModel looks like this:


class Rohdaten01importForm extends CFormModel

{


…


	/**

	 * Import the content of the file into the database.

	 *

	 * @param string $date in the form dd.mm.yyyy

	 * @param string $filename that is expanded with _yyyymmdd.db

	 * @param CDbConnection $dbConnection the connection to the database

	 * @return number of rows that are inderted in the database.

	 */

	public function import($date, $filename, CDbConnection $dbConnection=null)

	{


	    /**

	     * Insert Statement for the DAO. Later it should use the database

 	     * attributes from the model

	     */

	    $insertStatement = "INSERT INTO `tbl_rohdaten01` (`date`, `secofday`, `w2`, `resto2`, `aussentemp`, `vorlauf`, `boilerpump`, `boilertemp`) VALUES ";


	    // Convert the date in the form yyyymmdd and in the database form.

	    $date = Helper::GermanDate2Ymd($date);

	    $dbdate = substr($date, 0, 4) . '-' . substr($date, 4, 2) . '-' . substr($date, 6, 2);


	    // If we don't have assign database connectio, create it.

	    if (!isset($dbConnection))

	    {

	        $dbConnection = yii::app()->db;

	    }


	    // Construct the filename and extend _yyyymmdd.db

	    $filename = $filename . '_' . $date . '.db';


	    // Read the hole content of the file line by line

	    $fHandle = fopen($filename, 'r');

	    for($rownum=0; !feof($fHandle); $rownum++)

	    {

	        // Convert the row into columns

	        $line = fgets($fHandle);

	        $columns = explode(' ', rtrim($line));


	        // Create the SQL-Command with the connection.

	        $dbcommand = $dbConnection->createCommand($insertStatement . "( '$dbdate',$columns[0],$columns[1],$columns[2],$columns[3],$columns[4],$columns[5],$columns[6] )");


	        // Execute the Command

	        $dbcommand->execute();

	    }


	    // return the number of inserted rows.

	    return $rownum;


	}


…


}

Here you can see the import function, that read the content line by line, split the row into an array and call for each line the insert SQL-Statement.

In my testfunction testImport I want an unittets, that don’t need a database an no physical datafile. So I write an setup routine to manage the testdata from my “fixture”.


require(dirname(__FILE__) . '/../fixtures/HeizDrahtRohDaten01.init.php');

require_once 'vfsStream/vfsStream.php';


class Rohdaten01importFormTest extends CDbTestCase

{


    private $_form;

    private $_mockCDbConnection;

    private $_mockCDbCommand;


	/**

	 * @var string the virtuell datafile from vfsStream

	 */

	protected $_testFilename = 'vfs://testDir/HeizDrahtRohdaten01';




    /**

     * setUp initialize the test environment.

     *

     * @see CDbTestCase::setUp()

     */

    function setUp()

    {

        parent::setUp();


        // Create the form

        $this->_form = new Rohdaten01importForm();


        // Create the content for the datafile

	// 'vfs://testDir/HeizDrahtRohdaten01_yyyymmdd.db'

        // from the "fixture" HeizDrahtRohDaten01.init.php

        vfsStreamWrapper::register();


        $root = vfsStream::setup('testDir');


        $filename = $this->_testFilename . '_' . date('Ymd') . '.db';


        // Create the datafile in memory with content

        file_put_contents($filename, implode("\n", 

		HeizDrahtRohDaten01::getTestData()));


    }



After the setup I call the import function with my mockups for CDbConnection and CDbCommand, so I don’t need a real database system. I only test that the Yii functions ‘createCommand’ and ‘execute’ are called from the import function. That the function ‘createCommand’ and ‘execute’ are correct implemented from the Yii framework is naturally :rolleyes: .

This is the test function for the unittest (see inline comment).


    

    /**

     * The test function for Import

     */

    public function testImport()

    {


        // Define the expected rows that should be inserted.

        $expectedRows = HeizDrahtRohDaten01::getNumSets();


        // Create a MockUp of CDbConnection. We overwrite only the command

        // 'createCommand', because that

        // is the routine we call in the import function.

        $this->_mockCDbConnection = $this->getMock('CDbConnection',

		 array('createCommand'));


        // Create a MockUp of CDbCommand. We overwrite only the command

	// 'execute', because that is the routine we call in the import

        // function. 

	// We don‘t want to execute the originaly contructor of the class

	// of CdbCommand, so we need have to set the fifth parameter to false

        $this->_mockCDbCommand = $this->getMock('CDbCommand', array('execute'),

		 array(), '', false);


        // We need the date in dbDate format for the test

        $dbdate=date('Y-m-d');


        // For the first call we expected for the function call of

	// 'createCommand':

        //    a string with the insert statement and the values of the first

	//		line from the fixture,

        //    we return the object of the mockCDbCommand, so the import function

	//		can call our Mock execute.

        $this->_mockCDbConnection->expects($this->at(0))

             ->method('createCommand')

             ->with($this->equalTo("INSERT INTO `tbl_rohdaten01` (`date`, `secofday`, `w2`, `resto2`, `aussentemp`, `vorlauf`, `boilerpump`, `boilertemp`) VALUES ( '$dbdate',6,1,19,12,47,0,86 )"))

             ->will($this->returnValue($this->_mockCDbCommand));


        // For the second call we expected for the function call of

	// 'createCommand':

        //    a string with the insert statement and the values of the second

	//		line from the fixture,

        //    we return the object of the mockCDbCommand, so the import function

	  //		can call our Mock execute.

        $this->_mockCDbConnection->expects($this->at(1))

             ->method('createCommand')

             ->with($this->equalTo("INSERT INTO `tbl_rohdaten01` (`date`, `secofday`, `w2`, `resto2`, `aussentemp`, `vorlauf`, `boilerpump`, `boilertemp`) VALUES ( '$dbdate',66,2,19,20,30,0,87 )"))

             ->will($this->returnValue($this->_mockCDbCommand));


          /* 

	   * Here we can insert much more line for testing. 

	   */


        // This line ist special. If we change the numbers of rows for testing

	// in our fixture.

        // The import function call 'createCommand' more than two times. If we

	// don't have this line, the import function call 'createCommand' and

	// have no return of the object 'CDbCommand'.

        // The function call then null->execute() and we would have an error and

	// not a good result of the unittest.

        $this->_mockCDbConnection->expects($this->any())

             ->method('createCommand')

             ->will($this->returnValue($this->_mockCDbCommand));


        // This assert test if the command 'execute' of out mockCDbCommand is 	  

        // call of the correct quantity.

        $this->_mockCDbCommand->expects($this->exactly($expectedRows))

                              ->method('execute');


        // At this point we have the call of the function that we want to test.

        // We call the function with the actual date

        // and the datafile that is in memory

        // and not the Yii original CDbConnection, but our MockUp Object.

        $actrows = $this->_form->import(date('d.m.Y'), $this->_testFilename, $this->_mockCDbConnection);


        // At the end of the unittest we test the number of rows that are

        // inserted.

        $this->assertEquals($expectedRows, $actrows, 'not correct lines in file');


    }


}

To complete the example here is my "fixture" class for the testdata:


class HeizDrahtRohDaten01

{


    static $_data = array(

            'L1' => "6 1 19 12 47 0 86",

            'L2' => "66 2 19 20 30 0 87",

//             'L3' => "45 8 10 20 34 1 87",

            );


    static function getTestData()

    {

        return self::$_data;

    }


    static function getTestSet($name)

    {

        return self::$_data[$name];

    }


    static function getNumSets()

    {

        return count(self::$_data);

    }


}



I use eclipse PDT on Windows with XAMPP for this example.

Hope it helps someone!

//eRSTer2006