Testing Zendframework Controllers That Use the Post/Redirect/Get Plugin.
In order to protect against accidental re-posting of forms, we have employed the Post/Redirect/Get (PRG) page flow using the Zendframework plugin of the same name. Doing so introduced some interesting challenges in our test cases for controllers that used this flow.
The Problem
A typical PRG-based action method follows this pattern:
function myAction()
{
$prg = $this->prg('my-action');
if ($prg instanceof \Zend\Http\PhpEnvironment\Response) {
return $prg;
} elseif ($prg === false) {
return $view;
}
// Application logic here.
}
This structure detects the initial POST, stuffs its details into the user’s session and returns a redirect back to the requestor. The requestor follows the redirect and issues a GET back to this same action method which then passes through the PRG conditional logic and executes the application logic.
In order to properly test the application logic, we need to follow the same PRG flow as would happen in a live application. We cannot just dispatch the POST and GET requests in sequence as in-memory state might be maintained, something that would not occur with the live application, and we need to maintain the contents of the session since the PRG plugin relies on its contents to operate.
We do not, however, want to have to do this in every individual test case, as it would be a maintenance nightmare, never mind the tedium of having to code up two requests every time we need to test a PRG’d action method.
The Solution
Our solution boiled down (after a fair bit of digging!) to a single support method placed in a base class that PRG-based controller test cases can extend. This support method encapsulates the heavy lifting needed to dispatch a PRG request and can be reused by any test case that requires it.
The doPrg method also accepts an array of services which will be injected into Zend’s service locator. This allows test cases to mock up service objects to be used during test execution. These will be injected before both the POST and the GET requests.
Enough talk! Here’s the method implementation:
use \Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase as TestCase;
class HttpControllerTestCaseSupport extends TestCase
{
/**
* Execute a post/redirect/get request sequence.
* @param $url The URL to issue the initial POST to.
* @param $postParams An array containing the post parameters to send.
* @param $services An array (hash) of service names to services to
* inject into the application's service manager prior to dispatching
* both the POST and the GET requests. Note that the application is
* completely reinitialised between these two requests.
*/
protected function doPrg($url, $postParams, $services = array())
{
$serviceManager = $this->getApplicationServiceLocator();
foreach (array_keys($services) as $serviceName) {
call_user_func(array($serviceManager, 'setService'), $serviceName, $services[$serviceName]);
}
$this->dispatch($url, 'POST', $postParams);
$this->assertResponseStatusCode(303);
$redirectUri = $this->getResponseHeader('Location')->getUri();
$session = $_SESSION;
$cookie = $_COOKIE;
$this->reset();
$_COOKIE = $cookie;
$_SESSION = $session;
$serviceManager = $this->getApplicationServiceLocator();
foreach (array_keys($services) as $serviceName) {
call_user_func(array($serviceManager, 'setService'), $serviceName, $services[$serviceName]);
}
$this->dispatch($redirectUri, 'GET');
}
The use of this method effectively replaces a single call to
$this->dispatch($url)
in a non-PRG test case. On return, a test case can now
assert on the response using the standard Zend\Test assertions.
A simple example:
public function testSaveUser()
{
$this->doPrg('/saveUser', array('name' => 'Test User'));
$this->assertResponseCode(200);
$this->assertQuery('div#user');
}
Where a test case wishes to mock up some service calls expected to be executed by the action method under test, services can be mocked (or partially mocked) and provided to the doPrg method for injection into the Zend service locator:
use \Mockery as m;
public function testSetUserLanguage()
{
$serviceManager = $this->getApplicationServiceLocator();
$userManager = $serviceManager->get('UserManager');
$mockUserManager = m::mock($userManager)
->shouldReceive('setLanguage')
->with(893, 'fr')
->andReturn(true)
->getMock();
$_SESSION['user_id'] = '893';
$this->doPrg(
'/setLanguage',
array('language' => 'fr'),
array('UserManager' => $mockUserManager)
);
$this->assertResponseCode(200);
}
The Post/Redirect/Get page flow offers a better user experience when submitting forms to web applications but adds a layer of complexity when trying to unit test the action methods that use it. Our doPrg implementation has made testing these methods much easier for our dev team and anything that makes writing test cases easier for a developer can only be a Good Thing™.