-
Notifications
You must be signed in to change notification settings - Fork 142
Testing
Testing - best pratices with Cake2.x and PHPUnit3.7
In general it is best to only use testAction() once per test. My convention for the test method name is to use
"test" + "action name including prefix" + "method (if not default one)" + "optionally other pieces"
The following tests - using the Tools plugin MyControllerTestCase class - will per default use GET method unless specified otherwise:
public function testAdminAdd() {
$url = Router::url(array('admin' => true, 'controller' => 'users', 'action' => 'add'));
$options = array(
'return' => 'contents'
);
$result = $this->testAction($url, $options);
$this->assertNotEmpty($result);
}
public function testAdminAddPost() {
$url = Router::url(array('admin' => true, 'controller' => 'users', 'action' => 'add'));
$options = array(
'method' => 'post',
'return' => 'contents',
'data' => array(
'User' => array(
'name' => 'test'
)
)
);
$result = $this->testAction($url, $options);
$this->assertNull($result);
$url = Router::url(array('admin' => true, 'controller' => 'users', 'action' => 'index'));
$this->assertTextContains($url, $this->headers['Location']);
}
Example: TinyUrlsControllerTest.php
Note how I use dynamic URLs here to avoid routing changes affect all controller tests. So these tests effectively test the same URL as the frontend.
So if the following actions are available in a controller, basic tests should probably be added for
- index (GET)
- view (GET)
- add (GET + POST)
- edit (GET + POST)
- delete (POST)
and their admin counterparts, if applicable.
Using controller tests also tests the views to the actions. Which is a nice side effect. Any warning/error popping up there will then also be visible right away.
For plugin controllers to be testable unfortunately you always need to use $uses, even if it wasn't necessary due to conventions.
So for your Tools.TinyUrls
Controller you would need
public $uses = array('Tools.TinyUrl');
Otherwise it would always try to look for the model/fixture in your app dir, which eventually always fails.
Note: Do not forget to use --stderr
when testing in CLI. Otherwise all tests that use the session (and which work fine in webtest runner) will fail:
cake test app AllController --stderr
When testing controllers that need authentication (like admin actions), you can either mock the Auth component or simply write to the session to log in a specific user/role in setUp() or before using testAction():
CakeSession::write('Auth.User', array(
'username' => 'foo',
'role_id' => $roleId,
'id' => 1,
));
Don't forget to CakeSession::delete('Auth.User');
afterwards to avoid it affecting other tests.
It is also a good practice then to use this to clear the session prior to any test using setUp().
For testing AJAX controller actions, you need to set the right $_ENV keys ($_SERVER would also work):
$_ENV['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest';
$url = Router::url(array('controller' => 'posts', 'action' => 'update', 'ext' => 'json', '?' => array('value' => 2)));
$options = array(
'return' => 'vars'
);
$result = $this->testAction($url, $options);
When planning to upgrade 2.x apps to 3.x in the future, it is wise to look what will be ahead and act accordingly. The ControllerTestCase will be removed, and as such I backported the new IntegrationTestCase with basic functionality.
You can already use it for simple tests and save yourself more upgrading work later then:
public function testAdminAdd() {
$url = array('admin' => true, 'controller' => 'users', 'action' => 'add');
$this->get($url);
$this->assertResponseOk();
$this->assertNoRedirect();
}
And testing the POST:
public function testAdminAddPost() {
$url = array('admin' => true, 'controller' => 'users', 'action' => 'add');
$data = array(
'User' => array(
'name' => 'test'
)
);
$result = $this->post($url, $data);
$url = array('admin' => true, 'controller' => 'users', 'action' => 'index');
$this->assertResponseOk();
$this->assertRedirect($url);
}
In 2.x you would need to mock all three Console classes (Output, In, Out). I use TestConsoleOutput for the first, though, so I need to only mock the other two.
class PwdShellTest extends MyCakeTestCase {
public $PwdShell;
public function setUp() {
parent::setUp();
$output = new TestConsoleOutput();
$error = new TestConsoleOutput();
$input = $this->getMock('ConsoleInput', array(), array(), '', false);
$this->PwdShell = new PwdShell($output, $error, $input);
}
}
Example: PwdShellTest.php
The TestConsoleOutput.php can be used to capture, output and assert the stdout content:
// as array pieces
$output = $this->PwdShell->stdout->output;
// as string
$output = $this->PwdShell->stdout->output();
Make sure you are using ClassRegistry::init()
to init your models. This way they will automatically have the fixtures and db test config included.
Example: QloginTest.php
Make sure you include a View:
App::uses('View', 'View');
class LoremHelperTest extends CakeTestCase {
public $Lorem;
public function setUp() {
parent::setUp();
$this->Lorem = new LoremHelper(new View(null));
}
}
This is necessary when the helper needs any View attributes.
Example: LoremHelperTest.php
In some cases it might also be necessary to pass a Controller or even a request object:
$this->SomeHelper = new SomeHelper(new View(new Controller(new CakeRequest(null, false))));
I often just tested APIs directly in the past, not mocking them, but passing in username/key/token and directly asserting the results. This is bad in several ways for CI:
- You often get timeouts or connection errors when running multiple tests using travis and CO
- You make a lot of unnecessary API requests via testing
But then again I would not want to totally mock out everything, as that would mean I don't know when the "non-test environment" breaks due to changes in their API.
I had to find a DRY (Don't Repeat Yourself) approach. And I did, using --debug
flag of PHPUnit.
When applied it will make the live requests (run only locally), and by default it will mock out that part for CI.
A quick way of changing test vs live testing for a single test run and asserting all still goes well in reality.
// Inside a test case I can use the MyCakeTestCase::isDebug() helper
if (!$this->isDebug()) {
$Foo = $this->getMock('Foo', ...);
} else {
$Foo= new Foo();
}
$result = $Foo->bar();
A similar thing with debug output in general could be achieved using -v
or -vv
:
// Just an idea
if ($this->isVerbose()) {
debug($data);
}
But I like to use that internally via $this->debug()
from the MyCakeTestCase:
$this->debug($data);
It will not show by default, but once you apply one of the above verbose flags, you will see the output on the screen.
If you want to generate a HTML overview of all your locale test coverage:
cake test app AllApp --stderr --log-junit tmp/coverage/unitreport.xml --coverage-html tmp/coverage --coverage-clover tmp/coverage/coverage.xml
The report index.html will be in your /tmp/coverage folder.