RSS
 

Posts Tagged ‘Unit-Tests’

Unit-Testing Tips for 2.0 and PHPUnit

04 Dec

Quite some time ago I wrote about Unit testing. But that was still in 1.3 and with SimpleTest.
A lot has changed since then.

Execution Order

The documentation wasn’t all that clear about it. So I tried it with the following test file:

App::uses('MyCakeTestCase', 'Tools.Lib');
class TestCaseExecutionOrderTest extends MyCakeTestCase {
	public static function setUpBeforeClass() {
		parent::setUpBeforeClass();
		self::out('setUpBeforeClass');
	}
	
	public static function tearDownAfterClass() {
		parent::tearDownAfterClass();
		self::out('tearDownAfterClass');
	}
	
	public function setUp() {
		parent::setUp();
		
		$this->out('setUp');
	}
		
	public function tearDown() {
		$this->out('tearDown'); 
		
		parent::tearDown();
	}
	public function startTest() {
		$this->out('startTest');
	}
	public function endTest() {
		$this->out('endTest'); 
	}
	
	
	public function testFoo() {
		$this->out('* foo *');
	}
	
	public function testBar() {
		$this->out('* bar *'); 
	}
	
}

Note the parent calls for setUp and tearDown as well as setUpBeforeClass and tearDownAfterClass. Those need to be set. The other 2 methods don’t require this since they are not available in the parent class.
Also bear in mind that setUpBeforeClass and tearDownAfterClass need to be static. You cannot do dynamic stuff with it.

The result was pretty obvious:

setUpBeforeClass
setUp
startTest
* foo *
endTest
tearDown
setUp
startTest
* bar *
endTest
tearDown
tearDownAfterClass

Debug Output with PHPUnit >= 3.6

On the release notes of CakePHP2.0.3 you could find the note

A big difference people will notice when writing unit tests is that all output is swallowed by PHPUnit and not presented in either the web tester page nor in the CLI tester. To overcome this annoyance use the--debug modifier if you are using the CLI interface

Well, that broke a lot of existing debug code, of course.
So I tried to come up with a solution. The Shell uses a specific output(). Why not use sth like that in test cases, too?

/**
 * outputs debug information during a web tester (browser) test case
 * since PHPUnit>=3.6 swallowes all output by default 
 * this is a convenience output handler since debug() or pr() have no effect
 * @param mixed $data
 * @param bool $pre should a pre tag be enclosed around the output
 * @return void
 * 2011-12-04 ms
 */
public function out($data, $pre = true) {
	if ($pre) {
		pr($data);
	} else {
		echo $data;
	}
	if (empty($_SERVER['HTTP_HOST'])) {
		# cli mode / shell access: use the --debug modifier if you are using the CLI interface
		return;
	}
	ob_flush();
}

As you probably noticed, I use it in the above "execution tryout".
You can make yourself a custom "MyCakeTestCase" class with then extends the core test case class and put it in there.

UPDATE January 2012

The core (>=2.0) now contains a native way to enable debugging output for your tests.
You need to append &debug=1 to the url. Not ideal, but it works*.

  • As long as your tests run through. On error the debug output will still be swallowed and you need to use the above snippet or append ob_flush() to all debug output manually.
 
 

Unit-Testing in CakePHP

03 Apr

Warning: Some information my be deprecated! This is still from 1.3 and SimpleTest. CakePHP now uses PHPUnit in 2.x – please see the updated article here.

Some tips you might not have read about yet

Since the cookbook is quite detailed as far as basic testing is concerned I will not unroll that part too much. Please read those pages if you never wrote unit tests before.

Use your own wrapper

If you use your own testcase-wrapper you will be able to add some common functionality for all test cases – similar to AppModel, AppController or AppHelper.
I called it "my_cake_test_case.php" and put it in the vendors folder:

class MyCakeTestCase extends CakeTestCase {
	# added some time calc stuff - like calculating how long a test needed
	//
	# added some more custom stuff
	//
	# printing a header element like <h3> at the beginning of each test function
	function _header($title) {
		if (strpos($title, 'test') === 0) {
			$title = substr($title, 4);
			$title = Inflector::humanize(Inflector::underscore($title));
		}
		return '<h3>'.$title.'</h3>';
	}
	# overriding the assert rules in order to customize it (like adding detailed descriptions and before/after output)
	function assertEqual($is, $expected, $title = null, $value = null, $message = '%s', $options = array()) {
		$expectation = 'EQUAL';
		$this->_printTitle($expectation, $title, $options);
		$this->_printResults($is, $expected, $value, $options);
		return parent::assertEqual($is, $expected, $message);
	}
	function _printTitle($expectation, $title = null) {
		if (!$this->_reporter->params['show_passes']) {
			return false;
		}
		echo $this->_title($expectation, $title);
	}
	function _printResults($is, $expected, $pre = null, $status = false) {
		if (!$this->_reporter->params['show_passes']) {
			return false;
		}
		if ($pre !== null) {
			echo 'value:';
			pr ($pre);
		}
		echo 'result is:';
		pr($is);
		if (!$status) {
			echo 'result expected:';
			pr ($expected);
		}
	}
}

And much more

Now we can use it in the test cases

App::import('Vendor', 'MyCakeTestCase');
class ColorLibTest extends MyCakeTestCase {
	function testColDiff() {
		echo $this->_header(__FUNCTION__);
		$was = 'ffffff';
		$expected = 1;
		$is = $this->Color->colDiff($was, '000000');
		$this->assertEqual($is, $expected, null, $was));
	}
}

The header will be "ColDiff" – generated automatically using FUNCTION (magic constant).
The detailed messages are only displayed if "Show Passes" (url: show_passes=1) is activated. Otherwise it behaves as usual.

Skipping tests based on conditions

Sometimes tests depend on the system settings, enabled modules etc. If it doesn’t make sense to test certain methods, put this at the top of the test function:

# example with "magic_quotes_sybase"
function testSomething() {
	if ($this->skipIf(ini_get('magic_quotes_sybase') === '1', '%s magic_quotes_sybase is on')) {
		return;
	}
	... // this code will not be executed anymore
}

Always include object integrity first

Cake has a lot of automagic. Sometimes this can hurt, though, if unknown to the programmer.
If a model cannot be initialized it will usually fall back to AppModel – silently! Same with other classes.
So make sure, they are what they are supposed to be.

# this model is not an AppModel instance, but the real deal (Album instance of class Album)
function testAlbumInstance() {
	$this->assertTrue(is_a($this->Album, 'Album'));
}
# for a controller
function testAlbumsControllerInstance() {
	$this->assertTrue(is_a($this->Albums, 'AlbumsController'));
}

startCase() or startTest()

Most of the time startCase() is usually enough. This is triggered ONCE at the beginning of all tests. If the class itself does not remember or change anything, this is more than enough.
But as soon as your class (or object) internally keeps information about the last operation, you will get unexpected results. Use startTest() then to ensure, that the object is always a "fresh" one:

function startTest() {
	$this->Color = new ColorLib();
}

Note: setUp() is the same as startTest() but not officially used because it is SimpleTest specific.