Writing You Own PHPUnit Listener

Well, in the company, Parkyeri, we have restarted to a crusade to cover all the code lines we had by writing unit tests.

As a PHP Developer, I’m taking the part where I have to write PHPUnit, the xUnit family which is developed for the PHP language. I had used it in the first version for a while, but I had seen that it had reached to the 3rd version. As I’m someone who likes to use the latest things and demos, I tried the latest version and migrated all the first version unit tests to the latest version, which was surprisingly, very easy. You just have to change some class inheritance, delete some constructors, and you are nearly done.


The next part is to run the tests you have written, the best way, of course, is to use the CLI for PHPUnit. But as the Debian repositories does not include phpunit3, I can’t use CLI and I have to use something else.

So this is where we start to have trouble. I was thinking to show them on the browser but problems insisted. I will mention on them later. Let’s first create a testcase and then create a file to run my testcase. My testcase file is named ParkyeriTest.php:

require_once("PHPUnit/Framework/TestCase.php");

class ParkyeriTest extends PHPUnit_Framework_TestCase {

        function testMe() {
                $params = 1;
                $this->assertEquals(1, $params);
        }

        function testMeToo() {
                $params = 1;
                $this->assertEquals(2,$params);
        }
}

This will be our testcase. There is one test which fails and there is one which does not. The file will be called AllTests.php:

//Add the testcase you have created
require_once('ParkyeriTest.php');
//Don't miss to add TestSuite as we will instanciate it..
require_once('PHPUnit/Framework/TestSuite.php');
//And of course your own listener
require_once('myListener.php');
$suite = new PHPUnit_Framework_TestSuite(); //Create the instances
$mLis = new myListener();
$result = new PHPUnit_Framework_TestResult(); 
$suite->addTestSuite("ParkyeriTest"); //Add your test case to the test suite
$result->addListener($mLis); //Define your listener which the test result will use to give output
$suite->run($result); //And of course run this tests
//That's all buddies....

So you see that I have included my test case file and some PHPUnit files. I have created a TestSuite which will hold my TestCase and which will run it.

You see I have created a class named myListener. This class implements PHPUnit_Framework_TestListener and extends PHPUnit_Util_Printer. It implements TestListener because as a listener he has some jobs to do and this interface defines them, which are most basically things like addError(), addFailure() or startTest(). You will see them in the code block below. Let’s continue what we have done once we have instanciated our listener. We create another object PHPUnit_Framework_TestResult which holds the results of the test results. Also it allows you to listen the current process of the tests. I won’t mention the details of the TestResult, you can find them in PHPUnit Documentation.

And the rest is to add a listener to your results and then run the test suite. While the test suite ruins, you will se the output of your listener, as the execution progress. Now our listener’s file name is myListener.php:

require_once 'PHPUnit/Framework.php';
require_once 'PHPUnit/Util/Filter.php';
require_once 'PHPUnit/Util/Printer.php';
require_once 'PHPUnit/Util/Test.php';

//This part may confuse you. The only thing is, PHPUnit also contains a covered code part
//This line tells the PHPUnit to exclude it from code coverage things.
//At least that's what I understood :)
PHPUnit_Util_Filter::addFileToFilter(__FILE__, "PHPUNIT");

class myListener extends PHPUnit_Util_Printer implements PHPUnit_Framework_TestListener {
    protected $currentTestSuiteName = "";
    protected $currentTestName = "";
    protected $currentTestPass = TRUE;
    /**
     * Triggered when an error occurs on the test case
     */
    public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) {
        $this->writeCase(
          'error',
          $time,
          $e->getMessage()
        );

        $this->currentTestPass = FALSE;
    }
    /**
     * Triggered when one of the unit tests fails.
     */
    public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) {
        $this->writeCase(
          'fail',
          $time,
          $e->getMessage()
        );
        $this->currentTestPass = FALSE;
    }
    /**
     * Triggered when an incomplete test is encountered. 
     * You have to define the unit test as incomplete to trigger this.
     */
    public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) {
        $this->writeCase('error', $time, array(), 'Incomplete Test');
        $this->currentTestPass = FALSE;
    }
    /**
     * Triggered when an incomplete test is encountered. 
     * You have to define the unit test as to be skipped to trigger this.
     */
    public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) {
        $this->writeCase('error', $time, array(), 'Skipped Test');
        $this->currentTestPass = FALSE;
    }
    /**
     * Triggered when a testsuite is started
     */
    public function startTestSuite(PHPUnit_Framework_TestSuite $suite) {
        $this->currentTestSuiteName = $suite->getName();
        $this->currentTestName      = '';
        $this->write("Started Suite: " . $this->currentTestSuiteName . " (" . count($suite) . " tests)n");
    }
    /**
     * Triggered when a test suite ends.
     */
    public function endTestSuite(PHPUnit_Framework_TestSuite $suite) {
        $this->currentTestSuiteName = '';
        $this->currentTestName      = '';
    }
    /**
     * Triggered when a testcase starts
     */
    public function startTest(PHPUnit_Framework_Test $test) {
        $this->currentTestName = PHPUnit_Util_Test::describe($test);
        $this->currentTestPass = TRUE;
    }
    /**
     * Triggered when a testcase ends
     */
    public function endTest(PHPUnit_Framework_Test $test, $time) {
        if ($this->currentTestPass) {
            $this->writeCase('pass', $time);
        }
    }
    /**
     * I used this function to give output and not writing everything again and again.
     */
    protected function writeCase($status, $time, $message = '') { 
        $m = "Test: " . $this->currentTestName . " - Status: " . $status . " - Time: " . $time . ($message ? " - Message: " . $message: "") . "n";
        $this->write($m);
    }
}

I would have loved to add some PHPDoc, but I did not have much time.

What we have here is; we basically implement, I mean, create some functions that must be created in order to be used as a listener. Once you define this functions, you will decide what you will do with the current result. Basically, let’s take addFailure() as an example. This function is called whenever a test fails. Then you write what you want to do on that function and it’s done. You see that I have used another function writeCase(), which basically prints something on the screen by using, PHPUnit_Util_Printer‘s write() method. That’s why we have extended that class, because we had to give an output.

You may find additional listeners in the PHPUnit’s core of course. Something which will give you an xml output or a json output in root/PHPUnit/Util/Log folder.

Don’t forget to check out PHPUnit’s Manual and here for how to use PHPUnit_Util_Log_XML.

I have used PHPUnit_Util_Log_JSON as a base to my listener class by the way. You may want to check that one too. It had also implemented a complicated stack trace, but I didn’t need it a lot, as I show which test on which test suite I’m currently running and it’s not that hard to find the problem :)