Testing CodeIgniter 4 application with kahlan ^3.0
This is another post series about CodeIgniter and Kahlan, but now, it will uses CodeIgniter 4. CodeIgniter 4 requires some constants and required classes and functions to be included during application bootstrap to make front controller works.
For example, we need to do functional test App\Controllers\Home
controller, this is the kahlan-config.php we need to have:
<?php use CodeIgniter\CodeIgniter; use CodeIgniter\Services; use Config\App; use Config\Autoload; use Kahlan\Filter\Filter; Filter::register('ci.start', function($chain) { $root = $this->suite(); $root->beforeAll(function ($var) { define('ENVIRONMENT', 'testing'); define('BASEPATH', 'system' . DIRECTORY_SEPARATOR); define('APPPATH', 'application' . DIRECTORY_SEPARATOR); define('ROOTPATH', 'public' . DIRECTORY_SEPARATOR); define('WRITEPATH', 'writable' . DIRECTORY_SEPARATOR); define('CI_DEBUG', 1); require BASEPATH . 'Autoloader/Autoloader.php'; require APPPATH . 'Config/Constants.php'; require APPPATH . 'Config/Autoload.php'; require APPPATH . 'Config/Services.php'; class_alias('Config\Services', 'CodeIgniter\Services'); $loader = Services::autoloader(); $loader->initialize(new Autoload()); $loader->register(); require BASEPATH . 'Common.php'; $app = new App(); Services::exceptions($app, true)->initialize(); $var->codeIgniter = new CodeIgniter( $app ); }); return $chain->next(); }); Filter::apply($this, 'run', 'ci.start');
We now can call $this->codeIgniter property in all entire tests.
We can then place the spec under spec/ directory:
. ├── DCO.txt ├── README.md ├── application │ ├── Controllers │ │ └── Home.php ├── spec │ └── Controllers │ └── HomeDispatchSpec.php
We can apply $_SESSION['argv']
and $_SESSION['argc']
to assign URI routing data:
$_SERVER['argv'] = [ __FILE__, '/', // path ]; $_SERVER['argc'] = 2; ob_start(); $this->codeIgniter->run(); $actual = ob_get_clean(); expect($actual)->toContain('Welcome to CodeIgniter');
Here is the complete tests:
<?php namespace AppSpec\Controllers; describe('Home Dispatch', function () { describe('/', function () { it('contains "welcome" greeting', function () { $_SERVER['argv'] = [ __FILE__, '/', // path ]; $_SERVER['argc'] = 2; ob_start(); $this->codeIgniter->run(); $actual = ob_get_clean(); expect($actual)->toContain('Welcome to CodeIgniter'); }); }); });
Time to run test:
$ vendor/bin/kahlan --coverage=4 --src=application/Controllers/Home.php _ _ /\ /\__ _| |__ | | __ _ _ __ / //_/ _` | '_ \| |/ _` | '_ \ / __ \ (_| | | | | | (_| | | | | \/ \/\__,_|_| |_|_|\__,_|_| | | The PHP Test Framework for Freedom, Truth and Justice. Working Directory: /Users/samsonasik/www/CodeIgniter4 . 1 / 1 (100%) Expectations : 1 Executed Specifications : 0 Pending, 0 Excluded, 0 Skipped Passed 1 of 1 PASS in 0.120 seconds (using 4Mo) Coverage Summary ---------------- Lines % \ 1 / 1 100.00% └── App\ 1 / 1 100.00% └── Controllers\ 1 / 1 100.00% └── Home 1 / 1 100.00% └── Home::index() 1 / 1 100.00% Total: 100.00% (1/1) Coverage collected in 0.003 seconds (using an additionnal 16Ko)
Done 😉
Functional Test for Zend\Expressive Routed Middleware with Kahlan ^3.0
You may tried do functional test Zend\Expressive Routed Middleware and end up with “Unable to emit response; headers already sent” error.
This can happen because of during run test, the Test framework itself already run fwrite()
or echo
to build test report, and make the headers_sent()
return true.
To handle that, we can use ob_start()
, but since the header is sent in the background, we need to place in both places:
- test bootstrap
- before each test
Seriously? Yes! That’s make sure we only get Diactoros response that we use in the buffer to be tested.
Preparation
As usual, we need require kahlan/kahlan:^3.0 in require-dev:
$ composer require --dev kahlan/kahlan:^3.0 --sort-packages
Set Kahlan’s Bootstrap and before each globally
In Kahlan, we can set tests bootstrap and what in all before each test with Kahlan\Filter\Filter
in kahlan-config.php
, so we can write:
<?php //kahlan-config.php use Kahlan\Filter\Filter; ob_start(); Filter::register('ob_start at each', function($chain) { $root = $this->suite(); $root->beforeEach(function () { ob_start(); }); return $chain->next(); }); Filter::apply($this, 'run', 'ob_start at each');
Write Spec and Run In Action
Now, if we use Expressive skeleton application, and for example, we need to test App\Action\PingAction
routed middleware, we can write spec in spec directory:
. ├── composer.json ├── config ├── data ├── kahlan-config.php ├── public ├── spec │ └── App │ └── Action │ ├── PingActionDispatchSpec.php ├── src │ └── App │ └── Action │ ├── PingAction.php
As the App\Ping\PingAction
is return Zend\Diactoros\Response\JsonResponse
which contains “ack” data with time()
method call:
return new JsonResponse(['ack' => time()]);
The spec can be the following:
<?php namespace AppSpec\Action; use Zend\Diactoros\ServerRequest; use Zend\Expressive\Application; describe('PingAction Dispatch', function () { beforeAll(function() { $container = require 'config/container.php'; $this->app = $container->get(Application::class); }); describe('/api/ping', function () { it('contains json "ack" data', function () { allow('time')->toBeCalled()->andReturn('1484291901'); $serverRequest = new ServerRequest([], [], '/api/ping', 'GET'); $this->app->run($serverRequest); $actual = ob_get_clean(); expect($actual)->toBe('{"ack":"1484291901"}'); }); }); });
The ob_start()
will automatically called during test bootstrap and before each test.
Now, we can run the test:
$ vendor/bin/kahlan --coverage=4 --src=src/App/Action/PingAction.php _ _ /\ /\__ _| |__ | | __ _ _ __ / //_/ _` | '_ \| |/ _` | '_ \ / __ \ (_| | | | | | (_| | | | | \/ \/\__,_|_| |_|_|\__,_|_| | | The PHP Test Framework for Freedom, Truth and Justice. Working Directory: /Users/samsonasik/www/expressive . 1 / 1 (100%) Expectations : 1 Executed Specifications : 0 Pending, 0 Excluded, 0 Skipped Passed 1 of 1 PASS in 0.210 seconds (using 7Mo) Coverage Summary ---------------- Lines % \ 1 / 1 100.00% └── App\ 1 / 1 100.00% └── Action\ 1 / 1 100.00% └── PingAction 1 / 1 100.00% └── PingAction::__invoke() 1 / 1 100.00% Total: 100.00% (1/1) Coverage collected in 0.003 seconds (using an additionnal 0o)
Done 😉
Setup Java9 Early Access in Mac OS X
I have a chance to try latest Java9 development build 151, and I am very glad to have it successfully installed in my system. I am using OS X “El Capitan” (you may have latest: Mac OS X “Sierra” which I think will not be different for a way to install it). Here is the steps I did to get it works:
- Download the
.dmg
file fromhttps://jdk9.java.net/download/http://jdk.java.net/9/, select “Mac OS X” on “JDK” column (300+ MB file) - Install it
- Edit ~/.bash_profile and add the following line (if not exists)
#JAVA export JAVA_HOME=$(/usr/libexec/java_home) export PATH=$JAVA_HOME/bin:$PATH
- Apply ~/.bash_profile setting that has been modified by run command
$ source ~/.bash_profile
That’s it! Now, I can see it works:
leave a comment