How to Easily Setup PHPUnit Tests for Codeigniter 3

This tutorial will show you how to take a new or existing Codeigniter 3 project, and add PHPunit test capabilities to it. While Codeigniter 3 shipped with its very own unit test suite, some of us still prefer PHPUnit. I’ve read many methods online, but wasn’t 100% able to do everything I needed for one reason or another. This method aims to fix that.

In essence, we’re going to create a phpunit config file, tell Codeigniter when to use our test configurations, create a custom test route, and write an inert test controller to get the app in scope for our tests. Once those pieces are in place – just add tests! Although this is a longer post, I promise it’s pretty easy. I’m just a verbose kinda guy – apologies if you’re in a hurry.

Let’s get started!

Step 1: Defining our Testing ENVIRONMENT

In the Codeigniter 3 index.php file, you’ll see a bunch of logic that tries to determine and define the ENVIRONMENT. Here’s where we’re going to modify some of this logic to tell the app when we’re running tests so it knows to load the testing set of configs.

To do this, we’ll look in the index.php file around line 53 for something like this:

define('ENVIRONMENT', isset($_SERVER['CI_ENV']) ? $_SERVER['CI_ENV'] : 'development');

And replace it entirely with this:

if ( defined("PHPUNIT_TEST") ) {
    define('ENVIRONMENT', 'testing');
} else {
    define('ENVIRONMENT', isset($_SERVER['CI_ENV']) ? $_SERVER['CI_ENV'] : 'development');
}

Effectively, when PHPUNIT_TEST is defined, we’ll set the ENVIRONMENT constant to “testing”. This will tell Codeigniter to overwrite any production configs with what’s in the “testing” folder. I’ll show you how to create that to modify certain CI configurations for our tests shortly.

(Optional step) I’ve left the default environment determination (set as development) in there, but if you want, it might be wise to assume the app should run as production, JIC it gets deployed somewhere the CI_ENV isn’t defined properly and you don’t want errors to spew all over the front page after a deploy has gone sideways. Just replace the second define with this:

define('ENVIRONMENT', isset($_SERVER['CI_ENV']) ? $_SERVER['CI_ENV'] : 'production');

Just a thought though 😉

Step 2: Create a PHPUNIT Config File

Let’s assume that the Codeigniter 3 framework is installed in the root of your project. In this case, the index.php file, /application folder, /system folder, and maybe the /user_guide folder are all here. If this isn’t your setup, just know that the point here is to specify the Codigniter’s index.php file as the PHPUnit “bootstrap” file. For this tutorial, we’ll place our phpunit.xml file right here beside the index.php file and run our tests from that directory.

The contents of your phpunit.xml file should look something like this:

<?xml version="1.0" encoding="UTF-8" ?>
<phpunit bootstrap="index.php">
    <testsuites>
        <testsuite name="UnitTests">
            <directory>application/tests/unit</directory>
        </testsuite>
    </testsuites>
    <php>
        <ini name="display_errors" value="true"/>
        <const name="PHPUNIT_TEST" value="1" />
    </php>
</phpunit>

Here I’m making a few additional assumptions – all of which you can feel free to adjust to taste:

  1. We’re writing a unit test suite in this example
  2. We’ve placed our unit tests in the /application/tests/unit/ folder
  3. We want to display errors as they occur

If those 3 pieces are good with you, they’re good with me =]

You’ll also notice that there is a line that says <const name="PHPUNIT_TEST" value="1" /> in there – that’ll be defined when we use PHPUnit to invoke Codeigniter for our tests now!

Step 3: Configure Routes for Testing

Somehow we need to get the Codeigniter framework running and in scope for our tests. To do that, we’re going to create our “testing” environment’s routes.php config so we can tell Codeigniter where to route our testing requests (we’ll write the test controller in step 4).

Create a folder called “testing” inside the /application/config/ folder. Inside there, we’re going to overwrite the production routes.php by creating our own in /application/config/testing/routes.php. The inside of that file should look like this:

<?php

$route['default_controller'] = 'test/index';
$route['404_override'] = 'test/index'; // when in doubt, use the hammer
$route['translate_uri_dashes'] = FALSE;

At this point, feel free to create any other testing config files you need to overwrite any production values. Ensuring your tests aren’t running/accessing anything that could potentially *do things* should be paramount! Things you might want to consider are the /application/config/config.php values, the /application/config/database.php settings… etc.

Now we’re going to go create an empty Test controller to ensure our test request is inert.

Step 4: Creating Our Inert Test Controller

We want an empty controller for our tests. This does two things for us: 1) it gets the app within the scope of our tests so we can use the Codeigniter instance, and 2) it ensures that the controller doesn’t start doing actionable things to our data/system. In the case of our UnitTests suite, we only want to exercise the parts of the code we intend to.

Let’s create the test controller here: /application/controllers/Test.php and put the following code in it:

<?php

if ( !defined("PHPUNIT_TEST") ) {
    show_404();
}

class Test extends CI_Controller
{
    public function index()
    {
        // Yep... This is all we need. Like I said... Inert.
    }
}

Notice that we’ve placed a caveat at the top of this file. If PHPUNIT_TEST isn’t defined and /test is requested it’ll 404, or if this file is directly accessed, it’ll fatal with a “Undefined function: show_404()” message. Hopefully in production, that’s just a simple WSOD (white screen of death), but if someone’s pokin’ around here, a WSOD is totally acceptable… Of course, you are free to handle those situations as you see fit #GzipBomb =]

Also, I realize that maybe you have a MY_Controller class that you normally extend. Sometimes, it’s viable to skip extending that (and go straight to the CI_Controller) for your tests, and other times you might *need* to extend it. I’ve been in scenarios where I’ve had to extend the MY_Controller and overwrite methods and properties that do actionable things here in my Test controller (like user auth stuff, etc). Adjust to taste, but for this tutorial, we’ll stick with extending the CI_Controller.

Step 5: Writing Your First Test

Let’s create our first test by asserting that true is in fact true after we’ve pulled in the Codeigniter instance. Though trivial, this will help us ensure that our tests are running and we’ve got the framework at our fingertips. Create a test file here: /application/tests/unit/myFirstTest.php and we’ll put the following code inside:

<?php

use PHPUnit\Framework\TestCase;

class MyFirstTest extends TestCase
{
    private static $CI;

    public static function setUpBeforeClass(): void
    {
        self::$CI =& get_instance();
    }

    public function test_true(): void
    {
        $this->assertTrue(true);
    }
}

I’m using PHPUnit version 6.5.5 at the time of this writing, which requires that the methods specify their return data types. Also, this might not be backwards compatible if your PHPUnit version predates php 5.6 (IIRC), but you get the idea of what we’re trying to accomplish. Hit up your version of PHPUnit’s docs if you’re getting fatal errors extending the PHPUnit TestCase class like this.

Once this test file is in place, save it and lets run our tests from the CLI in the directory our phpunit.xml file is in:

>phpunit
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

.                                              1 / 1 (100%)

Time: 55 ms, Memory: 4.00MB

OK (1 test, 1 assertion)
>_

Heyyyyy! As it turns out, true is true – and running unit tests inside the context of CI is easy now too! You can use the CI instance to load your helpers/libraries/etc and exercise that code.

Sidenote: if you’ve been using PHPUnit for a while, you’re probably already familiar with the conventions, but JIC here they are:

  • file names of your tests end with “Test.php” (fooTest.php)
  • class names end with “Test” (class FooTest)
  • test methods START with “test”

If you’re getting a warning that no tests were found, make sure you have the conventions lined up and your test files are inside the folder you specified in the phpunit.xml file. In the example phpunit.xml file I gave, that folder is specified in the “UnitTests” TestSuite as /application/tests/unit and it *can* be inside a subfolder of that.

Bonus: Writing an Actual Test for a Pretend Library =]

Let’s look at what a real test for our app might look like. Let’s say we have a Hello library with a “hello world” method called who that we want to test. The library is appropriately placed in /application/libraries/Hello.php and our test file to exercise this code is in /application/tests/unit/libraries/helloTest.php. The code for our test could go something like this:

<?php

use PHPUnit\Framework\TestCase;

class HelloTest extends TestCase
{
    private static $CI;

    public static function setUpBeforeClass(): void
    {
        self::$CI =& get_instance();
        self::$CI->load->library('hello');
    }

    public function test_hello_world(): void
    {
        $expected = 'Hello World';
        $argument = 'World';
        // Here we test the Hello library's who method...
        $actual = self::$CI->hello->who($argument);
        $this->assertEquals($expected, $actual);
    }
}

This of course could exercise helper classes/functions that aren’t placed inside the CI instance class, or the tests could use a dataProvider – however you normally write your PHPUnit tests. Suffice to say, you still have access to using the framework almost exactly how you normally would inside your controllers.

Wrapping this up

So what all have we accomplished? We’ve created a way to flag Codeigniter to recognize we’re running tests, created a custom route config for our tests, built an inert controller to give PHPUnit access to our app, and wrote some tests!

I hope this tutorial was clear, helpful, and worked for you! Feel free to leave me any comments, questions, and/or feedback below.

Cheers!
Ryan

PS – sorry for waiting so long to write this. I hope it’s not too late.