Testing Controllers
Testing your controllers is made convenient with a couple of new helper classes and traits. When testing controllers, you can execute the code within a controller, without first running through the entire application bootstrap process. Often times, using the Feature Testing tools will be simpler, but this functionality is here in case you need it.
Note
Because the entire framework has not been bootstrapped, there will be times when you cannot test a controller this way.
The Helper Trait
To enable Controller Testing you need to use the ControllerTestTrait
trait within your tests:
<?php
namespace App\Controllers;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\ControllerTestTrait;
use CodeIgniter\Test\DatabaseTestTrait;
class FooControllerTest extends CIUnitTestCase
{
use ControllerTestTrait;
use DatabaseTestTrait;
}
Once the trait has been included, you can start setting up the environment, including the request and response classes,
the request body, URI, and more. You specify the controller to use with the controller()
method, passing in the
fully qualified class name of your controller. Finally, call the execute()
method with the name of the method
to run as the parameter:
<?php
namespace App\Controllers;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\ControllerTestTrait;
use CodeIgniter\Test\DatabaseTestTrait;
class ForumControllerTest extends CIUnitTestCase
{
use ControllerTestTrait;
use DatabaseTestTrait;
public function testShowCategories()
{
$result = $this->withUri('http://example.com/categories')
->controller(ForumController::class)
->execute('showCategories');
$this->assertTrue($result->isOK());
}
}
Helper Methods
controller($class)
Specifies the class name of the controller to test. The first parameter must be a fully qualified class name (i.e., include the namespace):
<?php
$this->controller(\App\Controllers\ForumController::class);
execute(string $method, …$params)
Executes the specified method within the controller. The first parameter is the name of the method to run:
<?php
$results = $this->controller(\App\Controllers\ForumController::class)
->execute('showCategories');
By specifying the second and subsequent parameters, you can pass them to the controller method.
This returns a new helper class that provides a number of routines for checking the response itself. See below for details.
withConfig($config)
Allows you to pass in a modified version of app/Config/App.php to test with different settings:
<?php
$config = new \Config\App();
$config->appTimezone = 'America/Chicago';
$results = $this->withConfig($config)
->controller(\App\Controllers\ForumController::class)
->execute('showCategories');
If you do not provide one, the application’s App config file will be used.
withRequest($request)
Allows you to provide an IncomingRequest instance tailored to your testing needs:
<?php
$request = new \CodeIgniter\HTTP\IncomingRequest(
new \Config\App(),
new \CodeIgniter\HTTP\URI('http://example.com'),
null,
new \CodeIgniter\HTTP\UserAgent()
);
$request->setLocale($locale);
$results = $this->withRequest($request)
->controller(\App\Controllers\ForumController::class)
->execute('showCategories');
If you do not provide one, a new IncomingRequest instance with the default application values will be passed into your controller.
withResponse($response)
Allows you to provide a Response instance:
<?php
$response = new \CodeIgniter\HTTP\Response(new \Config\App());
$results = $this->withResponse($response)
->controller(\App\Controllers\ForumController::class)
->execute('showCategories');
If you do not provide one, a new Response instance with the default application values will be passed into your controller.
withLogger($logger)
Allows you to provide a Logger instance:
<?php
$logger = new \CodeIgniter\Log\Handlers\FileHandler();
$results = $this->withResponse($response)
->withLogger($logger)
->controller(\App\Controllers\ForumController::class)
->execute('showCategories');
If you do not provide one, a new Logger instance with the default configuration values will be passed into your controller.
withUri(string $uri)
Allows you to provide a new URI that simulates the URL the client was visiting when this controller was run. This is helpful if you need to check URI segments within your controller. The only parameter is a string representing a valid URI:
<?php
$results = $this->withUri('http://example.com/forums/categories')
->controller(\App\Controllers\ForumController::class)
->execute('showCategories');
It is a good practice to always provide the URI during testing to avoid surprises.
Note
Since v4.4.0, this method creates a new Request instance with the URI.
Because the Request instance should have the URI instance. Also if the hostname
in the URI string is invalid with Config\App
, the valid hostname will be
set.
withBody($body)
Allows you to provide a custom body for the request. This can be helpful when testing API controllers where you need to set a JSON value as the body. The only parameter is a string that represents the body of the request:
<?php
$body = json_encode(['foo' => 'bar']);
$results = $this->withBody($body)
->controller(\App\Controllers\ForumController::class)
->execute('showCategories');
Checking the Response
ControllerTestTrait::execute()
returns an instance of a TestResponse
. See Testing Responses on
how to use this class to perform additional assertions and verification in your test cases.
Filter Testing
Similar to Controller Testing, the framework provides tools to help with creating tests for custom Filters and your projects use of them in routing.
The Helper Trait
Just like with the Controller Tester you need to include the FilterTestTrait
in your test
cases to enable these features:
<?php
namespace App\Filters;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FilterTestTrait;
class FooFilterTest extends CIUnitTestCase
{
use FilterTestTrait;
}
Configuration
Because of the logical overlap with Controller Testing FilterTestTrait
is designed to
work together with ControllerTestTrait
should you need both on the same class.
Once the trait has been included CIUnitTestCase
will detect its setUp
method and
prepare all the components needed for your tests. Should you need a special configuration
you can alter any of the properties before calling the support methods:
$request
A prepared version of the defaultIncomingRequest
service$response
A prepared version of the defaultResponseInterface
service$filtersConfig
The defaultConfig\Filters
configuration (note: discovery is handle byFilters
so this will not include module aliases)$filters
An instance ofCodeIgniter\Filters\Filters
using the three components above$collection
A prepared version ofRouteCollection
which includes the discovery ofConfig\Routes
The default configuration will usually be best for your testing since it most closely emulates a “live” project, but (for example) if you wanted to simulate a filter triggering accidentally on an unfiltered route you could add it to the Config:
<?php
namespace App\Filters;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FilterTestTrait;
final class FooFilterTest extends CIUnitTestCase
{
use FilterTestTrait;
protected function testFilterFailsOnAdminRoute()
{
$this->filtersConfig->globals['before'] = ['admin-only-filter'];
$this->assertHasFilters('unfiltered/route', 'before');
}
// ...
}
Checking Routes
The first helper method is getFiltersForRoute()
which will simulate the provided route
and return a list of all Filters (by their alias) that would have run for the given position
(“before” or “after”), without actually executing any controller or routing code. This has
a large performance advantage over Controller and HTTP Testing.
- getFiltersForRoute($route, $position)
- Parameters:
$route (
string
) – The URI to check$position (
string
) – The filter method to check, “before” or “after”
- Returns:
Aliases for each filter that would have run
- Return type:
string[]
Usage example:
<?php $result = $this->getFiltersForRoute('/', 'after'); // ['toolbar']
Calling Filter Methods
The properties describe in Configuration are all set up to ensure maximum performance without interfering or interference from other tests. The next helper method will return a callable method using these properties to test your Filter code safely and check the results.
- getFilterCaller($filter, $position)
- Parameters:
$filter (
FilterInterface|string
) – The filter instance, class, or alias$position (
string
) – The filter method to run, “before” or “after”
- Returns:
A callable method to run the simulated Filter event
- Return type:
Closure
Usage example:
<?php namespace App\Filters; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\FilterTestTrait; final class FooFilterTest extends CIUnitTestCase { use FilterTestTrait; protected function testUnauthorizedAccessRedirects() { $caller = $this->getFilterCaller('permission', 'before'); $result = $caller('MayEditWidgets'); $this->assertInstanceOf('CodeIgniter\HTTP\RedirectResponse', $result); } }
Notice how the
Closure
can take input parameters which are passed to your filter method.
Assertions
In addition to the helper methods above FilterTestTrait
also comes with some assertions
to streamline your test methods.
assertFilter()
The assertFilter()
method checks that the given route at position uses the filter (by its alias):
<?php
// Make sure users are logged in before checking their account
$this->assertFilter('users/account', 'before', 'login');
assertNotFilter()
The assertNotFilter()
method checks that the given route at position does not use the filter (by its alias):
<?php
// Make sure API calls do not try to use the Debug Toolbar
$this->assertNotFilter('api/v1/widgets', 'after', 'toolbar');
assertHasFilters()
The assertHasFilters()
method checks that the given route at position has at least one filter set:
<?php
// Make sure that filters are enabled
$this->assertHasFilters('filtered/route', 'after');
assertNotHasFilters()
The assertNotHasFilters()
method checks that the given route at position has no filters set:
<?php
// Make sure no filters run for our static pages
$this->assertNotHasFilters('about/contact', 'before');