Skip to content

Latest commit

 

History

History
511 lines (384 loc) · 15.5 KB

HowToWriteTests.md

File metadata and controls

511 lines (384 loc) · 15.5 KB

CI PHPUnit Test for CodeIgniter 3.0

version: v0.4.0 | v0.3.0 | v0.2.0

How to Write Tests

Introduction

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.

Testing Environment

Tests always run on testing environment.

If you don't know well about config files and environments, see CodeIgniter User Guide.

Can and Can't

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

MY_Loader

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.

exit()

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.

Reset CodeIgniter object

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.

Models

Using Database

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.

Database Seeding

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.

Using PHPUnit Mock Objects

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.

Controllers

Request to Controller

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.

Request to URI string

	public function test_uri_sub_sub_index()
	{
		$output = $this->request('GET', 'sub/sub/index');
		$this->assertContains('<title>Page Title</title>', $output);
	}

See working sample.

Request and Use Mocks

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.

Ajax Request

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.

Check Status Code

You can use $this->assertResponseCode() method in CI PHPUnit Test.

		$this->request('GET', 'welcome');
		$this->assertResponseCode(200);

Examine DOM in Controller Output

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.

Controller with Authentication

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.

redirect()

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);
	}

show_error() and show_404()

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.

Controller with Hooks

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.

Controller with Name Collision

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.

Mock Libraries

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.

More Samples

Want to see more tests?