version: v0.4.0 | v0.3.0 | v0.2.0
Here is my advice:
- You don't have to write your business logic in your controllers. Write them in your models.
- You should test models first, and test them well.
And PHPUnit has great documentation. You should read Writing Tests for PHPUnit.
Tests always run on testing
environment.
If you don't know well about config files and environments, see CodeIgniter User Guide.
CI PHPUnit Test does not want to modify CodeIgniter core files. The more you modify core, the more you get difficulities when you update CodeIgniter.
In fact, it uses a modified class and a few functions. But I try to modify as little as possible.
The functions and the class which are modified:
- function
load_class()
- function
is_loaded()
- function
is_cli()
- function
show_error()
- function
show_404()
- function
set_status_header()
- class
CI_Loader
They are in tests/_ci_phpunit_test/replacing
folder.
And CI PHPUnit Test adds a property dynamically:
- property
CI_Output::_status
CI PHPUnit Test replaces CI_Loader
and modifies below methods:
CI_Loader::model()
CI_Loader::_ci_load_library()
CI_Loader::_ci_load_stock_library()
But if you place MY_Loader, your MY_Loader extends the loader of CI PHPUnit Test.
If your MY_Loader overrides the above methods, probably CI PHPUnit Test does not work correctly.
CI PHPUnit Test does not care functions/classes which exit()
or die()
(Except for show_error() and show_404()).
So, for example, if you use URL helper redirect()
in your application code, your testing ends with it.
I recommend you not to use exit()
or die()
in your code. And you have to skip exit()
somehow in CodeIgniter code.
For example, you can modify redirect()
using MY_url_helper.php
in your application. I put a sample MY_url_helper.php. (I think CodeIgniter code itself should be changed testable.)
See redirect()
for details.
CodeIgniter has a function get_instance()
to get the CodeIgniter object (CodeIgniter instance or CodeIgniter super object).
CI PHPUnit Test has a new function reset_instance()
which reset the current CodeIgniter object. After resetting, you can (and must) create a new your Controller instance with new state.
tests/models/Inventory_model_test.php
<?php
class Inventory_model_test extends TestCase
{
public function setUp()
{
$this->CI =& get_instance();
$this->CI->load->model('shop/Inventory_model');
$this->obj = $this->CI->Inventory_model;
}
public function test_get_category_list()
{
$expected = [
1 => 'Book',
2 => 'CD',
3 => 'DVD',
];
$list = $this->obj->get_category_list();
foreach ($list as $category) {
$this->assertEquals($expected[$category->id], $category->name);
}
}
public function test_get_category_name()
{
$actual = $this->obj->get_category_name(1);
$expected = 'Book';
$this->assertEquals($expected, $actual);
}
}
Test case class extends TestCase class.
Don't forget to write parent::setUpBeforeClass();
if you override setUpBeforeClass()
method.
See working sample.
I put Seeder Library and a sample Seeder File.
They are not installed, so if you want to use, copy them manually.
You can use them like below:
public static function setUpBeforeClass()
{
parent::setUpBeforeClass();
$CI =& get_instance();
$CI->load->library('Seeder');
$CI->seeder->call('CategorySeeder');
}
See working sample.
You can use $this->getMockBuilder()
method in PHPUnit and $this->verifyInvoked*() helper method in CI PHPUnit Test.
If you don't know well about PHPUnit Mock Objects, see Test Doubles.
public function setUp()
{
$this->CI =& get_instance();
$this->CI->load->model('Category_model');
$this->obj = $this->CI->Category_model;
}
public function test_get_category_list()
{
// Create mock objects for CI_DB_pdo_result and CI_DB_pdo_sqlite_driver
$return = [
0 => (object) ['id' => '1', 'name' => 'Book'],
1 => (object) ['id' => '2', 'name' => 'CD'],
2 => (object) ['id' => '3', 'name' => 'DVD'],
];
$db_result = $this->getMockBuilder('CI_DB_pdo_result')
->disableOriginalConstructor()
->getMock();
$db_result->method('result')->willReturn($return);
$db = $this->getMockBuilder('CI_DB_pdo_sqlite_driver')
->disableOriginalConstructor()
->getMock();
$db->method('get')->willReturn($db_result);
// Verify invocations
$this->verifyInvokedOnce(
$db_result,
'result',
[]
);
$this->verifyInvokedOnce(
$db,
'order_by',
['id']
);
$this->verifyInvokedOnce(
$db,
'get',
['category']
);
// Replace property db with mock object
$this->obj->db = $db;
$expected = [
1 => 'Book',
2 => 'CD',
3 => 'DVD',
];
$list = $this->obj->get_category_list();
foreach ($list as $category) {
$this->assertEquals($expected[$category->id], $category->name);
}
// Reset CI object for next test case, unless property db won't work
reset_instance();
new CI_Controller();
}
See working sample.
You can use $this->request() method in CI PHPUnit Test.
tests/controllers/Welcome_test.php
<?php
class Welcome_test extends TestCase
{
public function test_index()
{
$output = $this->request('GET', ['Welcome', 'index']);
$this->assertContains('<title>Welcome to CodeIgniter</title>', $output);
}
}
See working sample.
public function test_uri_sub_sub_index()
{
$output = $this->request('GET', 'sub/sub/index');
$this->assertContains('<title>Page Title</title>', $output);
}
See working sample.
You can use $this->request->setCallable() method in CI PHPUnit Test. $this->getDouble() is a helper method in CI PHPUnit Test.
public function test_send_okay()
{
$this->request->setCallable(
function ($CI) {
$email = $this->getDouble('CI_Email', ['send' => TRUE]);
$CI->email = $email;
}
);
$output = $this->request(
'POST',
['Contact', 'send'],
[
'name' => 'Mike Smith',
'email' => 'mike@example.jp',
'body' => 'This is test mail.',
]
);
$this->assertContains('Mail sent', $output);
}
See working sample.
You can use $this->ajaxRequest() method in CI PHPUnit Test.
public function test_index_ajax_call()
{
$output = $this->ajaxRequest('GET', ['Ajax', 'index']);
$expected = '{"name": "John Smith", "age": 33}';
$this->assertEquals($expected, $output);
}
See working sample.
You can use $this->assertResponseCode() method in CI PHPUnit Test.
$this->request('GET', 'welcome');
$this->assertResponseCode(200);
I recommend to use symfony/dom-crawler.
$output = $this->request('GET', ['Welcome', 'index']);
$crawler = new Symfony\Component\DomCrawler\Crawler($output);
// Get the text of the first <h1>
$text = $crawler->filter('h1')->eq(0)->text();
See working sample.
I recommend to use PHPUnit mock objects. $this->getDouble() is a helper method in CI PHPUnit Test.
public function test_index_logged_in()
{
$this->request->setCallable(
$function ($CI) {
// Get mock object
$auth = $this->getDouble(
'Ion_auth', ['logged_in' => TRUE, 'is_admin' => TRUE]
);
// Inject mock object
$CI->ion_auth = $auth;
}
);
$output = $this->request('GET', ['Auth', 'index']);
$this->assertContains('<p>Below is a list of the users.</p>', $output);
}
See working sample.
I recommend to use this MY_url_helper.php.
If you use it, you can write tests like this:
public function test_index()
{
$this->request('GET', ['Admin', 'index']);
$this->assertRedirect('login', 302);
}
$this->assertRedirect() is a method in CI PHPUnit Test.
See working sample.
Upgrade Note for v0.4.0
v0.4.0 has new MY_url_helper.php
. If you use it, you must update your tests.
before:
/**
* @expectedException PHPUnit_Framework_Exception
* @expectedExceptionCode 302
* @expectedExceptionMessageRegExp !\ARedirect to http://localhost/\z!
*/
public function test_index()
{
$this->request('GET', ['Redirect', 'index']);
}
↓
after:
public function test_index()
{
$this->request('GET', ['Redirect', 'index']);
$this->assertRedirect('/', 302);
}
You can use $this->assertResponseCode() method in CI PHPUnit Test.
public function test_index()
{
$this->request('GET', ['nocontroller', 'noaction']);
$this->assertResponseCode(404);
}
See working sample.
If you don't call $this->request()
in your tests, show_error()
throws CIPHPUnitTestShowErrorException
and show_404()
throws CIPHPUnitTestShow404Exception
. So you must expect the exceptions. You can use @expectedException
annotation in PHPUnit.
Upgrade Note for v0.4.0
v0.4.0 has changed how to test show_error()
and show_404()
. You must update your tests.
before:
/**
* @expectedException PHPUnit_Framework_Exception
* @expectedExceptionCode 404
*/
public function test_index()
{
$this->request('GET', 'show404');
}
↓
after:
public function test_index()
{
$this->request('GET', 'show404');
$this->assertResponseCode(404);
}
If you don't want to update your tests, set property $bc_mode_throw_PHPUnit_Framework_Exception
true
in CIPHPUnitTestRequest class. But $bc_mode_throw_PHPUnit_Framework_Exception
is deprecated.
If you want to enable hooks, call $this->request->enableHooks() method. It enables pre_controller
, post_controller_constructor
, post_controller
hooks.
$this->request->enableHooks();
$output = $this->request('GET', 'products/shoes/show/123');
See working sample.
If you have two controllers with the exact same name, PHP Fatal error stops PHPUnit testing.
In this case, you can use PHPUnit annotations @runInSeparateProcess
and @preserveGlobalState disabled
. But tests in a separate PHP process are very slow.
tests/controllers/sub/Welcome_test.php
<?php
class sub_Welcome_test extends TestCase
{
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function test_uri_sub_welcome_index()
{
$output = $this->request('GET', 'sub/welcome/index');
$this->assertContains('<title>Page Title</title>', $output);
}
}
See working sample.
You can put mock libraries in tests/mocks/libraries
folder. You can see application/tests/mocks/libraries/email.php as a sample.
With mock libraries, you could replace your object in CodeIgniter instance.
This is how to replace Email library with Mock_Libraries_Email
class.
public function setUp()
{
$this->CI =& get_instance();
$this->CI->load->model('Mail_model');
$this->obj = $this->CI->Mail_model;
$this->CI->email = new Mock_Libraries_Email();
}
Mock library class name must be Mock_Libraries_*
, and it is autoloaded.
Want to see more tests?