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.

How I Write Professional Code

Subtitle: and talk about it UNprofessionally

I’ve been writing code for about the last decade and a half. It really wasn’t till I started working on a team of developers that I was pushed to write better, more readable code — a very important lesson. Then, it wasn’t till I started breaking old habits and started writing in a test driven development (or TDD) fashion that I started to write professional code. Continue reading “How I Write Professional Code”

Unmasking the User in PHP CLI Scripts

Who are you. Whoo whoo. Whoo whoo.

…The Who? Anyone? …Bueller?

So let’s say you need to know what user is running a script in PHP. If you’re like me, you may have tried get_current_user only to find that’s just for the current owner of the file. Well THAT’S deceivingly unhelpful! =\

Wait! Sec. One more moment of gripe: If what get_current_user in php does makes sense to you, I’d like to be the first to ask you: …How?!

Not only did that not help, but I also couldn’t find any solutions or hints in the right direction from the comments in the documentation (generally, I find them to be a good place for a next step). There was one suggesting to using posix functions like this: print_r(posix_getpwuid(posix_geteuid())); — however, no luck! If the user is invoking the script with sudo, we lose all trace of who done it. Also, this is technique can limit who can use it as it’s not uncommon for environments to disable posix_* functions for security.

So, Who Really Done It?!

Ranting aside… the answer to this eluded me! Low and behold, the it was inside of $_SERVER all along. Le sigh. I put together a quick function with a few fallbacks to figure it out. Let me know if this doesn’t work for you, but for me, does the trick in spades =]

function get_real_current_user()
{
    if (isset($_SERVER['SUDO_USER'])) {
        return $_SERVER['SUDO_USER'] . ' [priv]';
    }
    if (isset($_SERVER['LOGNAME'])) {
        return $_SERVER['LOGNAME'];
    }
    if (isset($_SERVER['HTTP_HOST'])) {
        // Could also use $_SERVER['REQUEST_URI'] rather than SCRIPT_NAME... up to you =]
        return "Web via: " . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'];
    }
    return "Unknown User";
}

I like this solution the best. Not only is this data already in the scope of our script already, but we don’t have to use posix or try crap like echo exec("who am i"); and parsing through that. #winning

Hope that helps someone out there in the ether!

Cheers!
Ryan

One or many

So, I get that there are probably better ways to handle this circumstance, but when working with legacy code, sometimes, it’s not up to you. I had a method that would be called with either one thing, or an array of these things. Approaching a context like this, the solution we came up with was (what I think is an) amazingly simple way to ensure if you’re sometimes receiving one thing or an array of things, you can handle both.

Check it out:


class Foo
{
    public function oneOrMany($input)
    {
        if ( is_array( $input ) ) {
            return array_map(array($this,'oneOrMany'), $input);
        }

        // Now do stuff to one $input only and return your results =]
        return $results;
    }
}

There it is. And now hopefully I’ll remember this better next time I need something quick.

Cheers!
Ryan

Quick way to remove all tables in MySQL DB through *nix CLI

Tablenator!

Here’s a quick time saver (if you dev like me, it’ll come in handy!) for when you just want to drop all of the tables in a database. Of course, test it first so you know what you’ll be dropping!

Test First:

for I in mysql database_name -u user_name -p -e 'show tables' | awk '{ print $1}' | grep -v '^Tables' ; do echo "Table: $I" ; done

Code:

for I in `mysql database_name -u user_name -p -e 'show tables' | awk '{ print $1}' | grep -v '^Tables'` ; do mysql -u user_name -p -e "drop table $I" ; done

Rundown

The basic formula is a BASH for loop that grabs all of the tables in the desired database, filters the output using a combination of AWK and GREP so we can use just a simple list, then drops each table separately.

USE THIS CODE AT YOUR OWN RISK! BACKUP YOUR DATABASE FIRST AND HAVE A RECOVERY PLAN IN PLACE! (trust a man known to fat finger things)

SQL Adding a Column to an Existing Table

Since I can never remember the syntax to add a column to a table, here it is:

alter table foo_tablename add column foo_column int(10) default null

  • alter table – the actual command
  • foo_tablename – the table name we’re altering
  • add column – telling sql to add a column instead of some other alteration
  • foo_column – name of the column to add
  • int(10) default null – the column’s qualities (it’s an int up to 10 digits long, can be null and defaults to null

Tada!!!

Performance Issue Gut Check for MySQL

Sometimes MySQL performance can be difficult to hammer out. One of the quickest ways to get a good gut check is to have a look at the current process list. Check this out:

mysql> show processlist;
+-------+------+-----------+------------+---------+------+----------+------------------+
| Id    | User | Host      | db         | Command | Time | State    | Info             |
+-------+------+-----------+------------+---------+------+----------+------------------+
| 14302 | abcd | localhost | rohjay_one | Sleep   | 3696 |          | NULL             |
| 14958 | abcd | localhost | NULL       | Query   |    0 | starting | show processlist |
+-------+------+-----------+------------+---------+------+----------+------------------+
2 rows in set (0.00 sec)

mysql>

Now, this is relatively uninteresting, but sometimes, you’ll find a list of active queries that have stacked up against your db. Some things that can really help you identify issues is by ensuring each different piece of your platform identifies itself uniquely.

Once that happens, you can see which queries from where are hanging on. Parts of your platform can stall and hold onto connections, queries can take an eternity, bad code can hold result sets in memory, etc… This should give you a good idea where to start looking!

Hope this helps =]