Tuesday 9 October 2012

Getting up & running with SimpleTest (2 of 2)

4.Reusing The Test Case

We can not only use the library assert functions ,but also can create our own assert functions. Also we can reuse test cases that we have defined. Next I would like to talk about how to define new assert functions and reuse test cases.

Firstly, take a look at our testClockTellsTime function which is defined in the clock_test.php. Assume we need to run the clock test hundred times, the test may be out by one second and would cause a failure. Therefore, I add the one second into our acceptable range. Create a new class called TimeTestCase into clock_test.php to fix that. And then, reusing the TimeTestCase.
class TimeTestCase extends UnitTestCase {
    function __construct($test_name=false) {                  
    //If leave it as blank, it will display the subclass's name   
        parent::__construct($test_name);
    }
    
    function assertSameTime($time1, $time2, $message=false) {
        if ( ! $message) {//If there is no message send in, it means that the test fail, show the message below.
           $message = "Time [$time1] should match time [$time2]"; 
        }
        
        $result = ($time1 == $time2) OR ($time1 + 1 == $time2);
        
        $this->assertTrue($result , $message);
                //add one second into our acceptable range 
        }
    }
}

class TestOfClock extends TimeTestCase {
    function TestOfClock() {
        $this->TimeTestCase('Clock class test');
    }
    function testClockTellsTime() {
        $clock = new Clock();
        $this->assertSameTime($clock->now(), time(), 'Now is the right time');
    }
    function testClockAdvance() {
        $clock = new Clock();
        $clock->advance(10);
        $this->assertSameTime($clock->now(), time() + 10, 'Advancement');
    }
}

Now, we can reuse the TimeTestCase.
There is a situation, if we combine this with other tests into our all_test.php. The system will recognize our reused test case as a individual case and test it. In order to prevent it, we could simply add abstract keyword to make the reused class as an abstract class that the system will not go into it. Another way is add "SimpleTestOptions::ignore('classname');" before the reused class, where the classname is reused class's name. In our case, we should add "SimpleTestOptions::ignore('TimeTestCase ');" before TimeTestCase class.

5.Using Mock Objects

Mock is a very powerful and useful technique in Unit Test. You can use a mock object to simulate the original class and can set arbitrary return values to test your function.
Firstly, I would like to use a simple example to explain how to use a mock object.
As you can see, we have set a log file that should be able to record some log information. Next, I will add a time stamp after the each log message.
Change the log.php as following
_file_path=$file_path;
   }

   function message($message){           

           $clock = new Clock();
           $file=fopen($this->_file_path,'a');
        fwrite($file,"[".$clock->now()."]$message.\n"); //add the time stamp to the log file
        fclose($file);    
   }
}


Then, if we test our log file by using log_test.php, the result will be failed. The reason is that we added the the stamp at the end of each log message, but we did not change our test program to match the new log message format. So we modify our log_test.php as following.
assertFalse(file_exists('temp/test.log'),'No file created before first message');
        $log->message('Should write this to a file');
        $this->assertTrue(file_exists('temp/test.log'),'File created');

    }
    function testAppendingToFile(){    
        $log=new Log('temp/test.log');
        $log->message('Test line 1');
        $message=file('temp/test.log');
        $this->assertPattern('/Test line 1/', $this->getFileLine('temp/test.log',0));
        $log->message('Test line 2');
        $message=file('temp/test.log');
        $this->assertPattern('/Test line 2/', $this->getFileLine('temp/test.log',1));    
    }

// new function to test time stamps    
    function testTimestamps(){
        $log = new Log('temp/test.log');  // create log file
        $log->message('Test line');       // add a log message

        $this->assertTrue(
            preg_match('/(\d+)/',$this->getFileLine('temp/test.log',0),$matches),'Found timestamp'  
            // test whether the digits are in the log message or not
        );
        $clock = new Clock();  
        $this->assertSameTime((integer)$matches[1],$clock->now(),'Correct time'); 
        //test whether the time stamp are as same as the current time  

    }
}
OK, now we run our test again. We should pass all tests now. And you should be able to see the time stamps matched in the pass message.
The time stamps are always changing, however, we do not want to test them every time if we knew they have no problems. Is there an easy way to bypass testing every second? Yes, we can use a mock object to solve this problem.
By using the mock object, we can set the return value to a constant, such as a string. Then, we only need to match the string rather than match the time stamps.
To use a mock object, we need add following code
<-- log_test.php -->
require_once ('simpletest/mock_objects.php');
Mock::generate('Clock');  // using Mock::generate('Class name') to create a mock object, where "Class name" is the class you want to mock it.  
Add these two lines before the class definition.
If you would like to check what Mock::generate() does, you could using print (Mock::generate()) to print it out.
Change the log_test.php again.
assertFalse(file_exists('temp/test.log'),'No file created before first message');
        $log->message('Should write this to a file');
        $this->assertTrue(file_exists('temp/test.log'),'File created');

    }
    function testAppendingToFile(){    
        $log=new Log('temp/test.log');
        $log->message('Test line 1');
        $message=file('temp/test.log');
        $this->assertPattern('/Test line 1/', $this->getFileLine('temp/test.log',0));
        $log->message('Test line 2');
        $message=file('temp/test.log');
        $this->assertPattern('/Test line 2/', $this->getFileLine('temp/test.log',1));

    }

    // Mock object

    function testTimestamps(){ 
        $clock = new MockClock($this);                       // mock object
        $clock->setReturnValue('now','Timestamp');           
        // mock object, set the return value to take place of the actual time stamp
         // The setReturnValue function is one of mock functions
         // There are also many other useful functions, 
         // you can print mock object out and check them 
        $log = new Log('temp/test.log');
        $log->message('Test line', $clock);
        $this->assertPattern('/Timestamp/',$this->getFileLine('temp/test.log',0), 'Found timestamp');  // only need to mathch the string 

        // we do not need those stuff anymore
        //$this->assertTrue(
        //    preg_match('/(\d+)/',$this->getFileLine('temp/test.log',0),$matches),'Found timestamp'
        //);
        //$clock = new Clock();
        //$this->assertSameTime((integer)$matches[1],$clock->now(),'Correct time');

    }
}
Now, run log_test.php again. You should be able to see, we did not exactly match the time stamps, but the results passed.
I just introduce one simplest way to use mock objects. When you generate a mock object to a class, the mock object actually creates a subclass for that class. All the functions of the original class are simulated by the mock object. You can set any return values for each function of the original class by using mock object in order to test what results you will get. You can also simulate the database connection rather than really connect database in your each testing.
For more details of using mock objects, please check simpletest's mock objects documentation.

Reference

This tutorial refer to the original tutorial at lastcraft.com ~ first test tutorial. All the code and ideas are come from the original tutorial. I just made that more clear and added some comments. If you are confused, please check the original tutorial.
The code in original tutorial worked on the old version. If you cannot follow the original tutorial, please check mine.


hope this will be helpful.
... continue reading!