How to Avoid –stderr When Running phpunit for Functional/Integration Testing
When you do a Functional/Integration test with session and/or header relation. It will force you to use --stderr
when running phpunit, or it will got error, eg: you’re testing that on logout when session exists as user, page will be redirected to login page with status code 302, and it got the following error:
$ vendor/bin/phpunit test/Integration/LogoutPageTest.php PHPUnit 8.5.2 by Sebastian Bergmann and contributors. Logout Page (AppTest\Integration\LogoutPage) ✘ Open logout page as auser redirect to login page ┐ ├ Failed asserting that 500 matches expected 302. │ ╵ /Users/samsonasik/www/mezzio-authentication-with-authorization/test/Integration/LogoutPageTest.php:36 ┴ Time: 155 ms, Memory: 10.00 MB FAILURES! Tests: 1, Assertions: 1, Failures: 1.
You can use --stderr
option on running it:
$ vendor/bin/phpunit test/Integration/LogoutPageTest.php --stderr PHPUnit 8.5.2 by Sebastian Bergmann and contributors. Logout Page (AppTest\Integration\LogoutPage) √ Open logout page as auser redirect to login page Time: 150 ms, Memory: 8.00 MB OK (1 test, 2 assertions)
or define stderr=true
in phpunit.xml
configuration:
<?xml version="1.0" encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" testdox="true" stderr="true"> <!-- testsuites, filter, etc config --> </phpunit>
Marking all test to be using stderr
is a workaround, as not all tests actually require that, eg: unit test doesn’t need that. To avoid it, we can define @runTestsInSeparateProcesses
and @preserveGlobalState disabled
in the controller class that require that, so, the test class will be like the following:
<?php declare(strict_types=1); namespace AppTest\Integration; use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\Uri; use Mezzio\Authentication\UserInterface; use PHPUnit\Framework\TestCase; /** * @runTestsInSeparateProcesses * @preserveGlobalState disabled */ class LogoutPageTest extends TestCase { private $app; protected function setUp(): void { $this->app = AppFactory::create(); } public function testOpenLogoutPageAsAuserRedirectToLoginPage() { $sessionData = [ 'username' => 'samsonasik', 'roles' => [ 'user', ], ]; $_SESSION[UserInterface::class] = $sessionData; $uri = new Uri('/logout'); $serverRequest = new ServerRequest([], [], $uri); $response = $this->app->handle($serverRequest); $this->assertEquals(302, $response->getStatusCode()); $this->assertEquals('/login', $response->getHeaderLine('Location')); } }
That’s it!
Publish Test Coverage to Codecov from Github Actions
Github Actions is one of ways to run Continues Integration. For Coverage report, we can use Codecov to publish the coverage result after running and generating test coverage.
For example, you have a Github Repository. You can open https://codecov.io/login and choose “Github”:
After you logged in, you can choose repository, or directly access https://codecov.io/gh/{your github user}/{your github repo}, for example, I use “samsonasik” as user, and “mezzio-authentication-with-authorization” as repository name:
https://codecov.io/gh/samsonasik/mezzio-authentication-with-authorization
On very first, we need to activate Webhook by open https://codecov.io/gh/gh/{your github user}/{your github repo}/settings, for example:
https://codecov.io/gh/samsonasik/mezzio-authentication-with-authorization/settings
Then, we click “Create new webhook” under Github Webhook:
After it done, we can copy “Repository Upload Token”:
by click “Copy” after then token, and back to Github, and save to Secrets section under Your Github Repository Settings with click “Add a new secret”, with eg: named: CODECOV_TOKEN, fill the value with your copied token, and click “Add secret” to save it to be like as follow:
The preparation is done. Now, time to add the github workflow, eg: “.github/workflows/ci-build.yml” at your repository, eg for php package/project and use phpunit, the workflow can be like the following:
name: "ci build" on: pull_request: push: branches: - "master" jobs: build: name: PHP ${{ matrix.php-versions }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: php-versions: ['7.2', '7.3', '7.4'] steps: - name: Setup PHP Action uses: shivammathur/setup-php@1.8.2 with: extensions: intl php-version: "${{ matrix.php-versions }}" coverage: pcov - name: Checkout uses: actions/checkout@v2 - name: "Validate composer.json and composer.lock" run: "composer validate" - name: "Install dependencies" run: "composer install --prefer-dist --no-progress --no-suggest && composer development-enable" - name: "Run test suite" run: "vendor/bin/phpunit --coverage-clover=coverage.xml" - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.xml flags: tests name: codecov-umbrella yml: ./codecov.yml fail_ci_if_error: true
That’s it. To trigger the Continues Integration, we can push a commit to the repository.
When its succeed (ci build green), we can then display coverage badge, eg at README.md like the following:
[](https://codecov.io/gh/{your github user}/{your github repo})
that will show:
Run test and coverage report with ant build in Windows
So, you want to run test and coverage report commands in Windows environment. For example, you want to run kahlan
command for test, then make coverage report by istanbul
and istanbul-merge
.
Based on the ant
documentation on exec part, it noted as follow:
The task delegates to Runtime.exec which in turn apparently calls ::CreateProcess. It is the latter Win32 function that defines the exact semantics of the call. In particular, if you do not put a file extension on the executable, only .EXE files are looked for, not .COM, .CMD or other file types listed in the environment variable PATHEXT. That is only used by the shell.
Note that .bat files cannot in general by executed directly. One normally needs to execute the command shell executable cmd using the /c switch.
If you are familiar with ant command, usually, you can create a build.xml
file as default build configuration. You can create another build file that you can mention it when run the ant
command, eg: build-windows.xml
:
$ ant -buildfile build-windows.xml
Your build-windows.xml
then can use “cmd” as executable
property with /c /path/to/executable-file
as argument. The file will like below:
<?xml version="1.0" encoding="UTF-8"?> <project name="Your Application name" default="build"> <property name="toolsdir" value="${basedir}/vendor/bin/"/> <property name="moduledir" value="${basedir}/module/"/> <target name="build" depends="kahlan,coverage-report" description=""/> <target name="kahlan" description="Run kahlan"> <!-- Application --> <exec executable="cmd" failonerror="true" taskname="kahlan"> <arg line="/c ${toolsdir}kahlan.bat --spec=${moduledir}Application/spec/ --src=${moduledir}Application/src --istanbul=coverage/coverage-application.json "/> </exec> <!-- Application --> <!-- other modules to be tested here --> </target> <target name="coverage-report" description="Run coverage report generation"> <!-- merge coverages for many modules test --> <exec executable="cmd" failonerror="true" taskname="istanbul merge"> <arg line="/c istanbul-merge --out coverage.json coverage/*.json"/> </exec> <!-- make report --> <exec executable="cmd" failonerror="true" taskname="istanbul report"> <arg line="/c istanbul report"/> </exec> </target> </project>
With above, when you run the command, if it succeed, it will likely as follow:
$ ant -buildfile build-windows.xml Buildfile: D:\app\build-windows.xml kahlan: [kahlan] _ _ [kahlan] /\ /\__ _| |__ | | __ _ _ __ [kahlan] / //_/ _` | '_ \| |/ _` | '_ \ [kahlan] / __ \ (_| | | | | | (_| | | | | [kahlan] \/ \/\__,_|_| |_|_|\__,_|_| | | [kahlan] [kahlan] The PHP Test Framework for Freedom, Truth and Justice. [kahlan] [kahlan] src directory : D:\app\module\Application\src [kahlan] spec directory : D:\app\module\Application\spec [kahlan] [kahlan] ...................................................... 11 / 11 (100%) [kahlan] [kahlan] [kahlan] [kahlan] Expectations : 11 Executed [kahlan] Specifications : 0 Pending, 0 Excluded, 0 Skipped [kahlan] [kahlan] Passed 11 of 11 PASS in 1.831 seconds (using 8Mo) [kahlan] [kahlan] Coverage Summary [kahlan] ---------------- [kahlan] [kahlan] Total: 100.00% (25/25) [kahlan] [kahlan] Coverage collected in 0.035 seconds coverage-report: [istanbul report] Done build: BUILD SUCCESSFUL Total time: 4 seconds
That’s it!
References:
Functional Test Symfony 4 with Kahlan 4
Yes, there is a bundle for it, but currently not fully work well with kahlan 4 yet. However, we can still use kahlan 4 for it. The simplest way is define Symfony 4 skeleton bootstrap in kahlan config, and use its property at specs, for example, we configure config at kahlan-config.php
as follows:
<?php // kahlan-config.php use App\Kernel; use Kahlan\Filter\Filters; use Symfony\Component\Debug\Debug; use Symfony\Component\HttpFoundation\Request; Filters::apply($this, 'bootstrap', function($next) { require __DIR__.'/config/bootstrap.php'; umask(0000); Debug::enable(); $root = $this->suite()->root(); $root->beforeAll(function () { $this->request = Request::createFromGlobals(); $this->kernel = new Kernel('test', false); }); return $next(); });
Above settings are minimal, if you need more setup, you can define there. If you didn’t require kahlan/kahlan:^4.0
, you can require via composer:
$ composer require --dev kahlan/kahlan:^4.0
Give a try
Let’s try testing a famous /lucky/number
from LuckyController
. We have the following controller:
<?php // src/Controller/LuckyController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Routing\Annotation\Route; class LuckyController extends Controller { /** * @Route("/lucky/number", name="lucky_number") */ public function number() { $number = mt_rand(0, 100); return $this->render('lucky/number.html.twig', [ 'number' => $number, ]); } }
And our twig file is:
{# templates/lucky/number.html.twig #} <h1>Your lucky number is {{ number }}</h1>
We can place test under spec
directory at root directory, for its test, we can create a spec/Controller
directory:
kahlan.config.php ├── spec │ └── Controller
Now, we can create the test as follows with make request to the ‘/lucky/number’ page and get its response. We can use toMatchEcho
matcher provided with regex to get match random number of mt_rand(0, 100)
that printed inside a response html content:
<?php // spec/Controller/LuckyControllerSpec.php namespace App\Spec\Controller; describe('LuckyController', function () { describe('/lucky/number', function () { it('shows lucky number', function () { $request = $this->request->create('/lucky/number', 'GET'); $response = $this->kernel->handle($request); expect(function () use ($response) { $response->send(); })->toMatchEcho( "#Your lucky number is ([0-9]|[1-8][0-9]|9[0-9]|100)#" ); }); }); });
Time to run it with command:
$ vendor/bin/kahlan
We will get the success output:
That’s it 😉
Testing Zend Expressive 2 with kahlan 3
Zend\Expressive ^2.0 has different default approach for piping and routing middleware which is programmatically way. In this post, I am going to show you how you can test Zend\Expressive ^2.0 application, with assumption, you use its skeleton with kahlan 3.
First, of course, install the Expressive ^2.0 skeleton, for example, install into directory named “expressive2”:
$ composer create-project zendframework/zend-expressive-skeleton:^2.0 expressive2 Installing zendframework/zend-expressive-skeleton (2.0.0) - Installing zendframework/zend-expressive-skeleton (2.0.0) Downloading: 100% Created project in expressive2 > ExpressiveInstaller\OptionalPackages::install Setting up optional packages Setup data and cache dir Removing installer development dependencies What type of installation would you like? [1] Minimal (no default middleware, templates, or assets; configuration only) [2] Flat (flat source code structure; default selection) [3] Modular (modular source code structure; recommended) Make your selection (2): 3
Now, install kahlan:
$ cd expressive2 $ composer require kahlan/kahlan:^3.1
We are going to need the $app
variable inside tests, for example, when testing functionality for each routed middlewares. To simplify and avoid repetitive code, we can register it into kahlan-config.php in root application:
// ./kahlan-config.php use Kahlan\Filter\Filter; use Zend\Expressive\Application; Filter::register('initialize app', function($chain) { $root = $this->suite(); ob_start(); $root->beforeAll(function ($var) { ob_start(); $var->app = $app = (require 'config/container.php')->get(Application::class); require 'config/pipeline.php'; require 'config/routes.php'; }); return $chain->next(); }); Filter::apply($this, 'run', 'initialize app');
By assign $app
into “$var->app” like above, the “$app” is accessible from entire tests via “$this->app”, so, we can write test like the following:
// ./src/App/spec/Action/HomePageActionSpec.php namespace AppSpec\Action; use Zend\Diactoros\ServerRequest; describe('HomePageAction', function () { describe('/', function () { it('contains welcome message', function () { $serverRequest = new ServerRequest([], [], '/', 'GET'); $this->app->run($serverRequest); $actual = ob_get_clean(); expect($actual)->toContain('Welcome to <span class="zf-green">zend-expressive</span>'); }); }); });
Now, let’s run the tests:
$ vendor/bin/kahlan --spec=src/App/spec/ _ _ /\ /\__ _| |__ | | __ _ _ __ / //_/ _` | '_ \| |/ _` | '_ \ / __ \ (_| | | | | | (_| | | | | \/ \/\__,_|_| |_|_|\__,_|_| | | The PHP Test Framework for Freedom, Truth and Justice. Working Directory: /Users/samsonasik/www/expressive2 . 1 / 1 (100%) Expectations : 1 Executed Specifications : 0 Pending, 0 Excluded, 0 Skipped Passed 1 of 1 PASS in 0.375 seconds (using 8Mo)
That’s it 😉
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 😉
Merge multiple coverages for Kahlan with istanbul merge
As you may already knew, you can generate HTML coverage report with kahlan with the following command:
$ ./bin/kahlan --src=path/to/src --spec=path/to/spec --istanbul="coverage.json" $ istanbul report
For multiple src path, with different specs location, as there are multiple coverages, we need to merge them. For example, we have the following application structure:
. └── module ├── A │ ├── spec │ │ └── ASpec.php │ └── src │ └── A.php └── B ├── spec │ └── BSpec.php └── src └── B.php
Prepare dependencies
1. Install the following tools:
2. Install kahlan/kahlan:^3.0
$ composer require kahlan/kahlan:^3.0 --dev --sort-packages
Here is the sample of composer.json
:
// composer.json { "name": "samsonasik/kahlan-demo", "type": "project", "require-dev": { "kahlan/kahlan": "^3.0" }, "autoload": { "psr-4": { "A\\": "module/A/src", "B\\": "module/B/src" } }, "autoload-dev": { "psr-4": { "ASpec\\": "module/A/spec", "BSpec\\": "module/B/spec" } }, "license": "MIT", "authors": [ { "name": "Abdul Malik Ikhsan", "email": "samsonasik@gmail.com" } ], "config": { "bin-dir": "bin" }, "minimum-stability": "dev", "prefer-stable": true }
Write specs
We can write specs under module/{FOLDER}/spec.
Define Tasks on build.xml
We need to register multiple commands in build.xml
- run bin/kahlan for module/A/src which set coverage target to coverage/coverge-A.json
- run bin/kahlan for module/B/src which set coverage target to coverage/coverge-B.json
- run instanbul-merge that merge coverage/coverage*.json to coverage.json
- run instanbul report
The build.xml can be like the following:
<?xml version="1.0" encoding="UTF-8"?> <project name="kahlan-demo app" default="build"> <property name="toolsdir" value="${basedir}/bin/"/> <property name="moduledir" value="${basedir}/module/"/> <target name="build" depends="kahlan,coverage-report" description=""/> <target name="kahlan" description="Run kahlan"> <!-- A --> <exec executable="${toolsdir}kahlan" failonerror="true" taskname="kahlan"> <arg line="--spec=${moduledir}A/spec/ --src=${moduledir}A/src --istanbul=coverage/coverage-A.json "/> </exec> <!-- A --> <!-- B --> <exec executable="${toolsdir}kahlan" failonerror="true" taskname="kahlan"> <arg line="--spec=${moduledir}B/spec/ --src=${moduledir}B/src --istanbul=coverage/coverage-B.json "/> </exec> <!-- B --> </target> <target name="coverage-report" description="Run coverage report generation"> <!-- merging coverage under coverage/ to coverage.json --> <exec executable="istanbul-merge" failonerror="true" taskname="istanbul merge"> <arg line="--out coverage.json coverage/*.json"/> </exec> <!-- generate report with use of merged coverages to coverage.json --> <exec executable="istanbul" failonerror="true" taskname="istanbul report"> <arg line="report"/> </exec> </target> </project>
Run tasks
We can run ant command and we will get the following output:
$ ant Buildfile: /Users/samsonasik/www/kahlan-demo/build.xml kahlan: [kahlan] _ _ [kahlan] /\ /\__ _| |__ | | __ _ _ __ [kahlan] / //_/ _` | '_ \| |/ _` | '_ \ [kahlan] / __ \ (_| | | | | | (_| | | | | [kahlan] \/ \/\__,_|_| |_|_|\__,_|_| | | [kahlan] [kahlan] The PHP Test Framework for Freedom, Truth and Justice. [kahlan] [kahlan] Working Directory: /Users/samsonasik/www/kahlan-demo [kahlan] [kahlan] . 1 / 1 (100%) [kahlan] [kahlan] [kahlan] [kahlan] Expectations : 1 Executed [kahlan] Specifications : 0 Pending, 0 Excluded, 0 Skipped [kahlan] [kahlan] Passed 1 of 1 PASS in 0.074 seconds (using 2Mo) [kahlan] [kahlan] Coverage Summary [kahlan] ---------------- [kahlan] [kahlan] Total: 100.00% (1/1) [kahlan] [kahlan] Coverage collected in 0.002 seconds (using an additionnal 70Ko) [kahlan] [kahlan] [kahlan] _ _ [kahlan] /\ /\__ _| |__ | | __ _ _ __ [kahlan] / //_/ _` | '_ \| |/ _` | '_ \ [kahlan] / __ \ (_| | | | | | (_| | | | | [kahlan] \/ \/\__,_|_| |_|_|\__,_|_| | | [kahlan] [kahlan] The PHP Test Framework for Freedom, Truth and Justice. [kahlan] [kahlan] Working Directory: /Users/samsonasik/www/kahlan-demo [kahlan] [kahlan] . 1 / 1 (100%) [kahlan] [kahlan] [kahlan] [kahlan] Expectations : 1 Executed [kahlan] Specifications : 0 Pending, 0 Excluded, 0 Skipped [kahlan] [kahlan] Passed 1 of 1 PASS in 0.045 seconds (using 2Mo) [kahlan] [kahlan] Coverage Summary [kahlan] ---------------- [kahlan] [kahlan] Total: 100.00% (1/1) [kahlan] [kahlan] Coverage collected in 0.001 seconds (using an additionnal 70Ko) [kahlan] [kahlan] coverage-report: [istanbul report] Done build: BUILD SUCCESSFUL Total time: 2 seconds
Now, we have successfully gotten merged coverage results with open coverage/lcov-report/index.html
:
Unit and Functional testing Zend Framework 3 Controller with Kahlan 3.0
This post will cover unit and functional testing ZF3 Controller with Kahlan 3.0. For example, you have a ZF3 Skeleton application with an IndexController
like the following:
namespace Application\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; class IndexController extends AbstractActionController { public function indexAction() { return new ViewModel(); } }
As usual, we need to require kahlan/kahlan:^3.0
via composer command:
composer require --dev kahlan/kahlan:^3.0 --sort-packages
You can then write the spec. Let’s write our spec inside module/Application/spec
like the following structure:
module/Application/ ├── config ├── spec │ ├── Controller │ │ ├── IndexControllerDispatchSpec.php │ │ └── IndexControllerSpec.php ├── src │ ├── Controller │ │ ├── IndexController.php
if we are only have the 1 module, named Application
module, we can define the spec
and src
path via kahlan-config.php like the following:
// ./kahlan.config.php $commandLine = $this->commandLine(); $commandLine->option('spec', 'default', 'module/Application/spec'); $commandLine->option('src', 'default', 'module/Application/src');
Or for multi-modules, we can run parallel command that specify --spec
and --src
in command like the following:
vendor/bin/kahlan --spec=module/Application/spec --src=module/Application/src
in each iteration. If you’re using ant
, you can write a build.xml
for tasks definition:
<?xml version="1.0" encoding="UTF-8"?> <project name="My Website" default="build"> <!-- executable files directory definition --> <property name="toolsdir" value="${basedir}/vendor/bin/"/> <!-- module directory definition --> <property name="moduledir" value="${basedir}/module/"/> <target name="build" depends="kahlan" description=""/> <target name="kahlan" description="Run kahlan"> <parallel> <!-- Application --> <exec executable="${toolsdir}kahlan" failonerror="true" taskname="kahlan"> <arg line="-spec=${moduledir}Application/spec/ --src=${moduledir}Application/src"/> </exec> <!-- Application --> <!-- other modules run test definition go here --> </parallel> </target> </project>
Unit testing
Let’s write the unit testing inside spec/Controller/IndexControllerSpec.php
:
namespace ApplicationSpec\Controller; use Application\Controller\IndexController; use Zend\View\Model\ViewModel; describe('IndexController', function () { given('controller', function () { return new IndexController(); }); describe('->indexAction()', function() { it('instance of ViewModel', function() { $actual = $this->controller->indexAction(); expect($actual)->toBeAnInstanceOf(ViewModel::class); }); }); });
That’s enough for IndexController::indexAction()
unit test, nothing complex logic we need to accomodate as it only return the ViewModel
instance, so we just need to check if return values is instance of ViewModel
.
Functional Testing
Now, we need to make sure if the dispatch response of IndexController::indexAction()
by open ‘/’ url that shown by user is the expected result, that show a welcome page, let’s do with spec/Controller/IndexControllerDispatchSpec.php
:
namespace ApplicationSpec\Controller; use Zend\Console\Console; use Zend\Mvc\Application; describe('IndexController Dispatch', function () { // setup the Application beforeAll(function () { Console::overrideIsConsole(false); $appConfig = include __DIR__ . '/../../../../config/application.config.php'; $this->application = Application::init($appConfig); $events = $this->application->getEventManager(); $this->application->getServiceManager() ->get('SendResponseListener') ->detach($events); }); // dispatch '/' page tests describe('/', function() { it('contains welcome page', function() { $request = $this->application->getRequest(); $request->setMethod('GET'); $request->setUri('/'); // run app with '/' url $app = $this->application->run(); // expect actual response is contain // a welcome page expect( $app->getResponse()->toString() )->toContain('<h1>Welcome to <span class="zf-green">Zend Framework</span></h1>'); }); }); });
That’s it 😉
Testing CodeIgniter application with Kahlan 3.0
Really? Yes, it is testable with kahlan
– The PHP Test Framework for Freedom, Truth, and Justice -. Let’s give a try, I am using CodeIgniter 3.1.0 for this example. You can download CodeIgniter from codeigniter.com . For example, we are going to test its Welcome
controller.
Setup:
a. require kahlan/kahlan
composer require --dev kahlan/kahlan:^3.0
b. setup minimal autoloading in kahlan-config.php
in root CodeIgniter project with Kahlan\Filter\Filter::register()
to register its autoloader:
<?php // ./kahlan.config.php use Kahlan\Filter\Filter; define('CI_VERSION', '3.1.0'); define('ENVIRONMENT', 'development'); define('APPPATH', 'application/'); define('VIEWPATH', 'application/views/'); define('BASEPATH', 'system/'); require_once BASEPATH . 'core/Common.php'; function &get_instance() { return CI_Controller::get_instance(); } Filter::register('ci.autoloader', function($chain) { $this->autoloader()->addClassMap([ // core 'CI_Controller' => BASEPATH . 'core/Controller.php', // controllers 'Welcome' => APPPATH . 'controllers/Welcome.php', ]); return $chain->next(); }); Filter::apply($this, 'namespaces', 'ci.autoloader');
c. Define the spec, we can create spec/controllers
directory for placing controller spec:
application/ spec/ └── controllers └── WelcomeSpec.php system/ kahlan-config.php
d. Write the spec:
<?php describe('Welcome', function () { describe('->index()', function () { it('contains welcome message', function() { $controller = new Welcome(); ob_start(); $controller->index(); $actual = ob_get_clean(); expect($actual)->toContain('Welcome to CodeIgniter!'); }); }); });
e. run the kahlan command
vendor/bin/kahlan --coverage=4 --src=application/
and you will get the following output:
What If we load model into controller ? How to test ?
We can also, For example, you have a model named Welcome_model
which check what passed name that will be used in controller:
<?php // application/models/Welcome_model.php class Welcome_model extends CI_Model { public function __construct() { parent::__construct(); } public function greeting($name) { if (trim($name) === '') { return 'Hello Guest'; } return 'Hello ' . $name; } }
As we need to check uri segment, we need to register new route in application/config/routes.php
:
$route['welcome/:name'] = 'welcome/index';
And now, we load in controller:
<?php // application/controllers/Welcome.php class Welcome extends CI_Controller { public function __construct() { parent::__construct(); $this->load->model('Welcome_model', 'welcome'); } public function index() { $greeting = $this->welcome->greeting($this->uri->segment(3)); $this->load->view('welcome_message', ['greeting' => $greeting]); } }
On view ( application/views/welcome_message.php
), we modify the greeting:
<?php // application/views/welcome_message.php ?> <h1><?php echo $greeting; ?>, Welcome to CodeIgniter!</h1>
At this case, we need a CI_URI::segment()
and Welcome_model::greeting()
to be stubbed in the spec, so, we need to modify our kahlan-config.php
to register CI_URI
, CI_Model
and its Welcome_model
classes:
<?php // ./kahlan-config.php use Kahlan\Filter\Filter; define('CI_VERSION', '3.1.0'); define('ENVIRONMENT', 'development'); define('APPPATH', 'application/'); define('VIEWPATH', 'application/views/'); define('BASEPATH', 'system/'); require_once BASEPATH . 'core/Common.php'; function &get_instance() { return CI_Controller::get_instance(); } Filter::register('ci.autoloader', function($chain) { $this->autoloader()->addClassMap([ // core 'CI_Controller' => BASEPATH . 'core/Controller.php', 'CI_URI' => BASEPATH . 'core/URI.php', 'CI_Model' => BASEPATH . 'core/Model.php', // controllers 'Welcome' => APPPATH . 'controllers/Welcome.php', // models 'Welcome_model' => APPPATH . 'models/Welcome_model.php', ]); return $chain->next(); }); Filter::apply($this, 'namespaces', 'ci.autoloader');
Then, here is the spec we will need to have:
<?php use Kahlan\Plugin\Double; describe('Welcome', function () { describe('->index()', function () { it('contains welcome message to specific passed name parameter', function() { define('UTF8_ENABLED', TRUE); // used by CI_Uri allow('is_cli')->toBeCalled()->andReturn(false); // to disable _parse_argv call // stubs CI_Uri::segment() $uri = Double::instance(['extends' => 'CI_URI']); allow($uri)->toReceive('segment')->with(3)->andReturn('samsonasik'); // stubs Welcome_model::greeting() $welcome_model = Double::instance(['extends' => 'Welcome_model']); allow($welcome_model)->toReceive('greeting') ->with('samsonasik') ->andReturn('Hello samsonasik'); $controller = new Welcome(); $controller->uri = $uri; $controller->welcome = $welcome_model; ob_start(); $controller->index(); $actual = ob_get_clean(); expect($actual)->toContain('Hello samsonasik, Welcome to CodeIgniter!'); }); }); });
As we are stubbing Welcome_model::greeting()
, here is the expected output that will be shown on run test:
.
If we want to make Welcome_model::greeting()
coverable, we can create a new spec for testing real Welcome_model::greeting()
call.
You wanna grab full sample? I created a repository for it so you can try: https://github.com/samsonasik/ci_310_with_kahlan 😉
Done 😉
Testing Hard Dependency with AspectMock
This is another testing legacy application post. Don’t tell your client/other people to refactor, if your job is only to make tests for it, as at some situations, there are reasons to not refactor. You may have situation to test hard dependency that impossible to be mocked and stubbed. There is a library named AspectMock for it, that you can use in PHPUnit, for example.
So, to have it, you can require via composer:
composer require "codeception/aspect-mock:^0.5.5" --dev
For example, you have the following class:
namespace App; class MyController { public function save() { $user = new User(); if (! $user->save()) { echo 'not saved'; return; } echo 'saved'; } }
That bad, huh! Ok, let’s deal to tests it even you don’t really like it. First, setup your phpunit.xml to have ‘backupGlobals=”false”‘ config:
<?xml version="1.0" encoding="UTF-8"?> <phpunit colors="true" backupGlobals="false" bootstrap="bootstrap.php"> <testsuites> <testsuite name="AppTest"> <directory suffix=".php">./test</directory> </testsuite> </testsuites> <filter> <whitelist addUncoveredFilesFromWhitelist="true"> <directory suffix=".php">./src</directory> </whitelist> </filter> </phpunit>
Now, based on config above, you can create bootstrap.php
:
include 'vendor/autoload.php'; use AspectMock\Kernel; $kernel = Kernel::getInstance(); $kernel->init([ 'debug' => true, 'cacheDir' => __DIR__ . '/data/cache', 'includePaths' => [__DIR__.'/src'], ]);
Assumption: You have ‘./data/cache’ for saving cache and ‘src/’ for your source code directory, if you use your own autoloader, you can add:
// ... $kernel->loadFile('YourAutoloader.php');
as the AspectMock documentation mentioned.
Now, time to write the tests:
- Preparation
namespace AppTest; use PHPUnit_Framework_TestCase; use App\MyController; use AspectMock\Test as test; class MyControllerTest extends PHPUnit_Framework_TestCase { private $myController; protected function setUp() { $this->myController = new MyController; } protected function tearDown() { test::clean(); // remove all registered test doubles } }
-
write the test cases
class MyControllerTest extends PHPUnit_Framework_TestCase { // ... public function provideSave() { return [ [true, 'saved'], [false, 'not saved'], ]; } /** * @dataProvider provideSave */ public function testSave($succeed, $echoed) { // mock $userMock = test::double('App\User', ['save' => $succeed]); ob_start(); $this->myController->save(); $content = ob_get_clean(); $this->assertEquals($echoed, $content); // stub $userMock->verifyInvoked('save'); } // ... }
Done 😉
references:
– https://github.com/Codeception/AspectMock
– https://twitter.com/grmpyprogrammer/status/642847787713884160
– https://littlehart.net/atthekeyboard/2014/12/14/stop-telling-me-to-refactor/
Monkey Patch PHP Quit Statement with Patchwork
If your job is make tests for legacy app that has exit()
or die();
everywhere, and you don’t have privilege to refactor them, make tests for them may be hard as the test aborted when the quit statements executed in the tests. To test them, we need to redefine the user-defined functions and methods at runtime, and there is a lib for that, it is named Patchwork.
We can run command:
$ composer require antecedent/patchwork:*
to get the Patchwork dependency.
Let’s see how it can work, let say, we have a class:
namespace App; class MyClass { public function foo($arg) { if ($arg === 1) { return true; } exit('app exit.'); } }
Seeing the MyClass::foo
, we can only tests if the $arg
is equal then 1, otherwise, we need to redefine it, and there is a Patchwork\replace()
method for it by call like the following:
replace(MyClass::class. '::foo', function($arg) { if ($arg === 1) { pass(); } return 'app exit.'; });
The pass()
method will call original method functionality if $arg === 1
as that not return quit statement, otherwise we redefine to return string with value = ‘app exit’.
We can define in our unit test like this MyClassTest
class:
namespace AppTest; use PHPUnit_Framework_TestCase; use App\MyClass; use function Patchwork\pass; use function Patchwork\replace; use function Patchwork\undoAll; class MyClassTest extends PHPUnit_Framework_TestCase { private $myclass; protected function setUp() { replace(MyClass::class. '::foo', function($arg) { if ($arg === 1) { pass(); } return 'app exit.'; }); $this->myclass = new MyClass; } protected function tearDown() { undoAll(); } }
We can call the replace
it in setUp()
, and to undo, we can call undoAll()
in tearDown()
. And now, we can add the tests into MyClassTest
tests :
// ... public function provideFoo() { return [ [1, true], [0, 'app exit.'], ]; } /** * @dataProvider provideFoo */ public function testFoo($arg, $result) { $this->assertSame($result, $this->myclass->foo($arg)); } // ...
References to read:
– http://kahlan.readthedocs.org/en/latest/monkey-patching/
– http://antecedent.github.io/patchwork/
– http://afilina.com/testing-methods-that-make-static-calls/
Testing Zend Framework 2 application using phpspec
If you’re going to start working with new ZF2 application, it is be a good chance to use phpspec for testing tool. We can describe specification and generate code that we already describe. Ok, let’s start with clone ZF2 skeleton application:
$ composer create-project zendframework/skeleton-application:dev-master zfnew
We will start application with “test first”, so, we can remove current module/Application’s classes:
$ cd zfnew $ rm -rf module/Application/Module.php $ rm -rf module/Application/src/Application/Controller/IndexController.php
The next step is setup requiring phpspec dependency and its needed extensions:
$ composer config bin-dir bin $ composer require phpspec/phpspec:~2.3.0 \ henrikbjorn/phpspec-code-coverage:~1.0.1 \ ciaranmcnulty/phpspec-typehintedmethods:~1.1 --dev
We will use henrikbjorn/phpspec-code-coverage
for code coverage generation, and ciaranmcnulty/phpspec-typehintedmethods
for typehint generation when running phpspec.
By default, our Application
module follow PSR-0 autoloader, so we need to define it in composer.json
:
// ... "autoload": { "psr-0": { "Application\\": "module/Application/src/" } }, // ...
To make it registered in composer’s autoload, we need to run dump-autoload:
$ composer dump-autoload
To point spec to describe Application namespace inside module/Application/src, we need to setup phpspec config under phpspec.yml, we can place it in root zfnew project:
# zfnew/phpspec.yml suites: application_suite: namespace: Application src_path: module/Application/src/ spec_path: module/Application extensions: - PhpSpec\Extension\CodeCoverageExtension - Cjm\PhpSpec\Extension\TypeHintedMethodsExtension code_coverage: format: - html - clover whitelist: - module/Application/src output: html: coverage clover: build/logs/clover.xml
Ok, let’s generate our first spec:
$ bin/phpspec desc Application/Module
We will get output like the following:
We will get generated first spec like the following:
// module/Application/spec/Application/ModuleSpec.php namespace spec\Application; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class ModuleSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('Application\Module'); } }
And when we run:
$ bin/phpspec run
We will get generated class like this if we choose ‘Y’ answering “Do you want me to create Application\Module
for you?” question:
And we will get a Module class inside module/Application/src/Application
directory:
namespace Application; class Module { }
We need to have more examples to achieve standard Module class, that has getConfig()
method, and especially for Application
module, we need onBootstrap(MvcEvent $e)
method, so we can write examples like the following:
// module/Application/spec/Application/ModuleSpec.php namespace spec\Application; use Application\Module; use PhpSpec\ObjectBehavior; use Zend\EventManager\EventManager; use Zend\Mvc\Application; use Zend\Mvc\MvcEvent; class ModuleSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType(Module::class); } function it_return_config() { $getConfig = $this->getConfig(); $getConfig->shouldBeArray(); $getConfig->shouldReturn( include __DIR__ . '/../../config/module.config.php' ); } function its_bootstrap(MvcEvent $e, $application, $eventManager) { $application->beADoubleOf(Application::class); $eventManager->beADoubleOf(EventManager::class); $application->getEventManager()->willReturn($eventManager)->shouldBeCalled(); $e->getApplication()->willReturn($application)->shouldBeCalled(); $this->onBootstrap($e); } }
And when run, we will get the following errors:
Do you want me to create `Application\Module::getConfig()` for you? [Y/n] Y Method Application\Module::getConfig() has been created. Do you want me to create `Application\Module::onBootstrap()` for you? [Y/n] Y Method Application\Module::onBootstrap() has been created. Application/Module 18 - it return config is_array(null) expected to return true, but it did not. Application/Module 28 - its bootstrap some predictions failed: Double\Zend\Mvc\MvcEvent\P2: No calls have been made that match: Double\Zend\Mvc\MvcEvent\P2->getApplication() but expected at least one. Double\Zend\Mvc\Application\P1: No calls have been made that match: Double\Zend\Mvc\Application\P1->getEventManager() but expected at least one. 33% 66% 3 1 specs 3 examples (1 passed, 2 failed) 708ms
Don’t worry about it, it is normal, we just need to fulfill what already described in code as we have generated code template:
namespace Application; class Module { public function getConfig() { // TODO: write logic here } public function onBootstrap(\Zend\Mvc\MvcEvent $mvcEvent) { // TODO: write logic here } }
let’s fill it so it looks like:
namespace Application; use Zend\Mvc\ModuleRouteListener; class Module { public function getConfig() { return include __DIR__ . '/../../config/module.config.php'; } public function onBootstrap(\Zend\Mvc\MvcEvent $mvcEvent) { $eventManager = $mvcEvent->getApplication()->getEventManager(); $moduleRouteListener = new ModuleRouteListener(); $moduleRouteListener->attach($eventManager); } }
To prove, you can re-run bin/phpspec run
and everything will be green ;).
Now, let’s create spec for Application\Controller\IndexController
:
$ bin/phpspec desc Application/Controller/IndexController
And we can define the IndexControllerSpec:
// module/Application/spec/Application/Controller/IndexControllerSpec.php namespace spec\Application\Controller; use Application\Controller\IndexController; use PhpSpec\ObjectBehavior; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; class IndexControllerSpec extends ObjectBehavior { function let(ViewModel $viewModel) { $this->beConstructedWith($viewModel); } function it_is_initializable() { $this->shouldHaveType(IndexController::class); } function it_is_extends_abstract_action_controller() { $this->shouldBeAnInstanceOf(AbstractActionController::class); } function its_index_action_return_view_model(ViewModel $viewModel) { $this->indexAction()->shouldReturn($viewModel); } }
We use beConstructedWith()
, so, we need to inject ViewModel into controller’s construction. We can run bin/phpspec run
and we will get the following code:
// module/Application/src/Application/Controller/IndexController.php namespace Application\Controller; class IndexController { public function __construct(\Zend\View\Model\ViewModel $viewModel) { // TODO: write logic here } public function indexAction() { // TODO: write logic here } }
Let’s fulfill the examples as described in spec:
// module/Application/src/Application/Controller/IndexController.php namespace Application\Controller; use Zend\Mvc\Controller\AbstractActionController; class IndexController extends AbstractActionController { private $viewModel; public function __construct(\Zend\View\Model\ViewModel $viewModel) { $this->viewModel = $viewModel; } public function indexAction() { return $this->viewModel; } }
We need to build the Controller with Factory, so we can describe the factory:
$ bin/phpspec desc Application/Factory/Controller/IndexControllerFactory
We can write spec examples:
// module/Application/spec/Application/Factory/Controller/IndexControllerFactorySpec.php namespace spec\Application\Factory\Controller; use Application\Controller\IndexController; use Application\Factory\Controller\IndexControllerFactory; use PhpSpec\ObjectBehavior; use Zend\ServiceManager\FactoryInterface; use Zend\ServiceManager\ServiceLocatorInterface; class IndexControllerFactorySpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType(IndexControllerFactory::class); } function it_is_implements_factory_interface() { $this->shouldImplement(FactoryInterface::class); } function it_is_create_indexcontroller(ServiceLocatorInterface $serviceLocator) { $this->createService($serviceLocator) ->shouldReturnAnInstanceOf(IndexController::class); } }
When run bin/phpspec run
, we will get generated code:
// module/Application/src/Application/Factory/Controller/IndexControllerFactory.php namespace Application\Factory\Controller; class IndexControllerFactory { public function createService(\Zend\ServiceManager\ServiceLocatorInterface $serviceLocatorInterface) { // TODO: write logic here } }
Let’s modify to fulfill the spec examples:
// module/Application/src/Application/Factory/Controller/IndexControllerFactory.php namespace Application\Factory\Controller; use Application\Controller\IndexController; use Zend\ServiceManager\FactoryInterface; use Zend\View\Model\ViewModel; class IndexControllerFactory implements FactoryInterface { public function createService(\Zend\ServiceManager\ServiceLocatorInterface $serviceLocatorInterface) { $viewModel = new ViewModel(); return new IndexController($viewModel); } }
So, everything looks good, we can run bin/phpspec run
again, and we can get green result again:
We can see the coverage result in coverage/index.html
:
Now, to make our ZF2 application still works when call ‘/’ in browser, we can update our module/Application/config/module.config.php
:
// ... 'controllers' => array( 'factories' => array( 'Application\Controller\Index' => 'Application\Factory\Controller\IndexControllerFactory' ), ), // ...
That’s it 😉
PHPUnit: Testing Closure passed to Collaborator
Yes! Closure is callable, so you can just call __invoke()
to the closure returned when test it! This is happen when we, for example, have a class and function that have closure inside it like the following:
class Awesome { public function call($foo) { return function() use ($foo) { return $foo; }; } }
This can be tested with:
use Awesome; use PHPUnit_Framework_TestCase; class AwesomeTest extends PHPUnit_Framework_TestCase { protected function setUp() { $this->awesome = new Awesome(); } public function testCall() { $foo = 'foo'; $call = $this->awesome->call($foo); $this->assertTrue(is_callable($call)); $invoked = $call->__invoke(); $this->assertEquals($foo, $invoked); } }
We need an __invoke()
call, as the closure never executed before it invoked. So, we need to call that.
On Collaborator Case
The problem is when we have a collaborator, and closure is processed inside the collaborator:
class Awesome { private $awesomeDependency; public function __construct(AwesomeDependency $awesomeDependency) { $this->awesomeDependency = $awesomeDependency; } public function call($foo) { $closure = function() use ($foo) { return $foo; }; return $this->awesomeDependency->call($closure); } }
and the closure inside call only executed in the AwesomeDependency
class:
class AwesomeDependency { public function call($call) { return $call(); } }
Our test can be like the following:
use Awesome; use AwesomeDependency; use PHPUnit_Framework_TestCase; class AwesomeTest extends PHPUnit_Framework_TestCase { protected function setUp() { $this->awesomeDependency = $this->prophesize(AwesomeDependency::class); $this->awesome = new Awesome($this->awesomeDependency->reveal()); } public function testCall() { $foo = 'foo'; $closure = function() use ($foo) { return $foo; }; $this->awesomeDependency ->call($closure) ->will(function() use ($closure) { return $closure->__invoke(); }) ->shouldBeCalled(); $call = $this->awesome->call($foo); } }
As we can see, the $this->awesomeDependency
is act as a mock, and calling __invoke()
in will()
is represent a $closure
that already passed to the mock, not the original $closure
one, and we will get partial coverage:
We know now, it won’t coverable as completed! What we can do? A refactor! But wait, it may be a legacy code, an aggressive refactor may cause problem, so a little but works patch may work for it.
- Make a
$closure
as class property, and add mutator and accessor for it.
class Awesome { private $closure; // ... public function setClosure($closure) { $this->closure = $closure; } public function getClosure() { return $this->closure; } // ... }
- Set
$closure
property when callcall()
function:
class Awesome { // ... public function call($foo) { $this->setClosure(function() use ($foo) { return $foo; }); return $this->awesomeDependency->call($this->getClosure()); } }
- And in tests, we can now has:
class AwesomeTest extends PHPUnit_Framework_TestCase { // ... public function testCall() { $foo = 'foo'; $closure = function() use ($foo) { return $foo; }; $awesome = $this->awesome; $this->awesomeDependency ->call($closure) ->will(function() use ($awesome) { return $awesome->getClosure()->__invoke(); }) ->shouldBeCalled(); $call = $this->awesome->call($foo); $this->assertEquals($foo, $call); } }
Need a better way? We can replace a closure with an array callback, so, we add additional function that called via call_user_func_array()
:
class Awesome { public function call($foo) { return $this->awesomeDependency->call(call_user_func_array( [$this, 'onFoo'], [$foo] )); } public function onFoo($foo) { return function() use ($foo) { return $foo; }; } }
And in our tests, we can do:
public function testCall() { $foo = 'foo'; $closure = function() use ($foo) { return $foo; }; $awesome = $this->awesome; $this->awesomeDependency->call($closure) ->will(function() use ($awesome, $foo) { return $awesome->onFoo($foo)->__invoke(); }) ->shouldBeCalled(); $call = $this->awesome->call($foo); $this->assertEquals($foo, $call); }
And, we now have a fully coverage too:
Testing “expects($this->any())” with Prophecy with Spying
Testing method call from Collaborator that may be 0 or any other count with phpunit Framework test case can be done with expects($this->any())
from mock object. If we are using Prophecy for mocking tools, it can be done with spying
.
However, the spying itself need checks whenever method is called or not. We need findProphecyMethodCalls()
call againsts ObjectProphecy for that.
For example, we have a class with collaborator like the following:
namespace App; class Awesome { private $awesomeDependency; public function __construct(AwesomeDependency $awesomeDependency) { $this->awesomeDependency = $awesomeDependency; } public function process($data) { $rand = rand(1, 2); if ($rand === 1) { return $this->awesomeDependency->process($data); } return $data; } }
The rand()
usage is just a sample, in real app, we may have a heavy logic and it may fall to not call the collaborator.
The tests can be done like this:
namespace AppTest; use App\Awesome; use App\AwesomeDependency; use PHPUnit_Framework_TestCase; use Prophecy\Argument; use Prophecy\Argument\ArgumentsWildcard; class AwesomeTest extends PHPUnit_Framework_TestCase { protected function setUp() { $this->awesomeDependency = $this->prophesize(AwesomeDependency::class); $this->awesome = new Awesome($this->awesomeDependency->reveal()); } public function testProcess() { $data = [ 'foo' => 'abcdef', ]; // make \Prophecy\MethodProphecy instance $methodProphecy = $this->awesomeDependency ->process(Argument::exact($data)); // call method from actual instance $this->awesome->process($data); $calls = $this->awesomeDependency->findProphecyMethodCalls( 'process', new ArgumentsWildcard([$data]) ); $count = count($calls); // assert how many times it called $methodProphecy->shouldBeCalledTimes($count); if ($count) { // echoing just to prove echo 'Method from collaborator has been called'; $methodProphecy->shouldHaveBeenCalled(); } else { // echoing just to prove echo 'Method from collaborator has not been called'; $methodProphecy->shouldNotHaveBeenCalled(); } } }
Of course, it may be can’t be called ‘expecting’ before for something has done, it may be can be called as ‘recording’ what already happen, but by this usage, we can prove if it actually called in actual code.
leave a comment