Automatic testing of MVC applications created with Zend Framework

This article contains information on how to improve your application stability by controlling how the changes affect the Model-View-Controller Zend Framework application. As a result, you will be able to deliver better software to your customers and reduce the time needed for testing of the software.

Testing should be automatic

The problem of all changes in a software application is that they can damage it. The effect can be various, from hardly-detected change in complex calculation to just a "big crash" when user just runs it or, in case of the Web application, types site URL in browser. There is no exception to this rule and disregarding application size it should be checked after any change. It is possible for small applications to just manually repeat some use cases and test that everything works as expected. But if application testing requires more than a few minutes then only few of us have enough obstinacy to repeat this boring and monotonous work after each change. The idea of automatizing the process comes into mind just after second such check and, hopefully, there are enough robust and powerful automatic testing solutions, also known as unit testing frameworks. They provide developers with tools and guidelines on how to test software automatically.

Program tests program

Different software requires different testing. It is obvious that the price of a software error is different and what can be required for medical institutions can be just unacceptable for a small content management system. For one function or one class the better solution may be just a file with a few calls of functions and methods and just print('OK') at the end but more complex systems require a different methods and tools. Anyway, the idea is the same:

  • you are writing another program to test your program
  • this program does not require user interaction to run and
  • in the time of execution it creates a log or report how tests are passed
  • you run it when you want to test your application

MVC improves testability

What affects a testing process is structure or architecture of the application. Bad architecture usually can be detected by impossibility to select isolated testing areas, like user interface, data model, utility classes, and so on. If you noticed the magic MVC keyword in the title you may suppose me to talk about model-view-controller and you are right. MVC is a great software pattern that simplifies not only development but testing as well by separating application into three parts that can be tested individually.

MVC application creating is easy with Zend Framework

Creating MVC application with Zend Framework takes significally less time then creating it without the framework. Not only because ZF has a good helpers for that but also because you do not have a chance to do things wrong if you use Zend_Controller, Zend_Db and Zend_View classes. This triad joins all the best practices available to most skilled PHP developers from Zend team. The details on how to create an application that is based on MVC pattern is out of scope of this article and if you want to look at example, refer to Zend Framework Manual.

Select and modify data to test Model

Model of a ZF application encapsulates access to data storage and relations between data items. In 80-90% of all cases it maps classes to the database tables of the application database (with some additional domain logic such as data validation that cannot be implemented in database) and in 20-10% of all cases the model includes some adapters to other information sources like active directory, files and so forth. So, model is an interface for loading and modifying of the application data and model tests usually include various select, insert, update and delete operations.

View is simple but unsteady

View is very simple by their nature. Zend Framework manual View examples contains plain PHP scripts with almost no logic in them and this is what makes wrong impression that it does not require a special attention in tests. But simplicity of View is compensated by its instability--View is a most frequently changed part of a MVC application and wrong View creating strategy may cause View tests very hard to maintain. The solution is simple: separate decoration and structure of (X)HTML document using CSS files and try to not modify renderers.

Testing View includes testing Ajax

Another aspect of View testing is Ajax (formerly known as DHTML). Application page may be perfectly rendered on the server, but opening it in browser may cause a crash or blank screen. And what is worst, it is almost impossible to test Ajax using PHP frameworks for automatic tests because PHP just cannot execute JavaScript. Some artificial solutions exist (like loading a browser using the COM PHP extension) but no practical one. So be ready to your tests may contain server code tests with PHPUnit and client code tests with Selenium.

Use cases show how to test Controller

Application is created for users and developers usually understand now the application is supposed to be used. At least they recommend using it in some way. And what is good in Web is that whatever users do with application their steps can be recorded in a sequence of HTTP requests and that greatly simplifies creating tests for controllers and their actions. So creating Controller tests is simple but depends on Model and View tests. Actually, you may do not have special tests for Model and View but Controller tests are what your automatic tests script should contain.

Example

The following example shows how to test part of the application that handles new user registrations and singing in. Let's say that we have the "user" table in database, the "Users" class that extends Zend_Db_Table, the "UserController" class that is a controller and it contains five view actions: register, signIn, signOut, info, error; and three processing actions registerDo, singInDo, signOutDo. In terms of MVC, Users is Model of the application, UserController is Controller and actions uses corresponding View scripts to display forms and data.

There are a lot of unit tests frameworks available but, as usual, only few of them are really used by the community. The most famous are PHPUnit and SimpleTest and I prefer to use PHPUnit just because it is used by Zend Framework development team.

The file structure of the application is following:

/
  protected/
      application/
          main/
              controllers/
                  UserController.php
              models/
                  Users.php
              views/
                  scripts/
                      user/
                          register.phtml
                          sign.in.phtml
                          sign.out.phtml
                          info.phtml
                          message.phtml
      database/
          db.sql
          db.sqlite
      tests/
          application/
              main/
                  controllers/
                      UserControllerTests.php
                  models/
                      UsersTests.php
          database/
              db.sqlite
          AllTests.php
  .htaccess
  index.php

There is one module in the application and structure of the "tests" folder duplicates the file system structure of the "application" folder to simplify locating of a test file that tests particular functionality. "index.php" is a "bootstrap" file, which initializes framework and run the front controller. .htaccess contains mod_rewrite rule that redirects all requests to index.php. There are no View tests because View will be tested as a part of Controller.

Making application feature-complete requires a lot of work and is not a goal for this article. So let's only review simple user registration form.

index.php

set_include_path('.' . PATH_SEPARATOR . '../../htlibs');

require_once 'Zend/Loader.php';
spl_autoload_register(array('Zend_Loader', 'autoload'));

$db = Zend_Db::factory('PDO_SQLITE', array('dbname' =>  dirname(__FILE__) 
    . '/protected/database/db.sqlite'));
Zend_Db_Table::setDefaultAdapter($db);

$front = Zend_Controller_Front::getInstance();
$front->addControllerDirectory('protected/application/main/controllers', 'main');
$front->throwExceptions(true);

$front->dispatch();
protected/application/main/controllers/UserController.php

class Main_UserController extends Zend_Controller_Action {

    function registerAction() {
        $this->render();
    }

    function registerDoAction() {
        require_once dirname(__FILE__) . '/../models/Users.php';
        $users = new Users();
        $user = $users->fetchNew();
        $user->name = $this->_getParam('name', '');
        $user->password = $this->_getParam('password', '');
        try {
            $user->save();
            $this->_setParam('message', 'User is registered');
        } catch (Exception $e) {
            $this->_setParam('message', $e->getMessage());
        }
        $this->_forward('message');
    }

    function messageAction() {
        $view = $this->initView();
        $view->message = $this->_getParam('message', 'Unknown errror');
        $this->render();
    }

}
protected/application/main/models/Users.php

class Users extends Zend_Db_Table
{
    protected $_name = 'user';
}
protected/application/main/views/scripts/user/register.phtml

<h1>Registration form</h1>
<form action="<?=$this->url(array('action' => 'register.do'))?>" method="post">
Name: <?=$this->formText('name', '')?><br>
Password: <?=$this->formPassword('password', '')?><br>
<?=$this->formSubmit('submit', '')?><br>
</form>
protected/application/main/views/scripts/user/message.phtml

<h1>System message</h1>
<?=$this->escape($this->message)?>
protected/database/db.sql

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` INTEGER PRIMARY KEY,
  `name` VARCHAR(50) NOT NULL,
  `password` VARCHAR(50) NOT NULL
);
CREATE UNIQUE INDEX `user_name` ON `user` (`name`);

This very small application supports only one use case:

  1. User opens http://localhost/main/user/register
  2. System shows registration screen by invoking registerAction() of Main_UserController. This action renders the "register.phtml" View script.
  3. User enters desired name and password into the form fields and submits form.
  4. System sends data to registerDo action and then forwards request to the "message" action with operation status.

Tests infrastructure requires a few steps to be made to start creating tests. These steps are described well in PHPUnit documentation so I only point them as follows:

  • Download PHPUnit.
  • Install its libraries into the folder with server side libraries.
  • Create main test file, called AllTests.php. It will be used to run tests for the application functionality.

For our small application AllTests.php will be similar to the "index.php" bootstrap file but with additional includes and AllTests class, that creates TestSuite and adds tests into it.

protected\tests\AllTests.php

set_include_path('.' . PATH_SEPARATOR . '../../../htlibs');

require_once 'Zend/Loader.php';
spl_autoload_register(array('Zend_Loader', 'autoload'));

$db = Zend_Db::factory('PDO_SQLITE', array('dbname' =>  dirname(__FILE__) 
    . '/database/db.sqlite'));
Zend_Db_Table::setDefaultAdapter($db);

$front = Zend_Controller_Front::getInstance();
$front->addControllerDirectory('protected/application/main/controllers', 'main');
$front->throwExceptions(true);

require_once 'application/main/controllers/UserControllerTests.php';
require_once 'application/main/models/UsersTests.php';

class AllTests {

    public static function main() {
        PHPUnit_TextUI_TestRunner::run(self::suite(), array());
    }

    public static function suite() {
        $suite = new PHPUnit_Framework_TestSuite();
        $suite->setName('SampleApp');
        $suite->addTestSuite('Main_UserControllerTests');
        $suite->addTestSuite('UsersTests');
        return $suite;
    }
}

AllTests::main();

"Main_UserControllerTests" and "UsersTests" are the names of the classes defined in UserControllerTests.php and UsersTests.php as follows:

protected\tests\application\main\controllers\UserControllerTests.php

class Main_UserControllerTests extends PHPUnit_Framework_TestCase {

    function setUp() {
        // Prepare the database
        $db = Zend_Db_Table::getDefaultAdapter();
        $db->query('DELETE FROM user');
    }

    function testMainUseCase() {
        // User opens http://localhost/main/user/register 
        $front = Zend_Controller_Front::getInstance();
        $request = new Zend_Controller_Request_Http('http://localhost/main/user/register');
        $response = new Zend_Controller_Response_Http();
        $front->returnResponse(true)->setRequest($request)->setResponse($response);
        // System shows registration screen by invoking registerAction().
        // This action renders the "register.phtml" View script. 
        $front->dispatch();
        $this->assertContains('</form>', $response->getBody());
        // User enters desired name and password into the form fields 
        // and submits form. 
        $request = new Zend_Controller_Request_Http('http://localhost/main/user/register.do');
        $request->setParams(array('name' => 'joe', 'password' => 'secret'));
        $response = new Zend_Controller_Response_Http();
        $front->returnResponse(true)->setRequest($request)->setResponse($response);
        // System sends data to registerDo action and then forwards request 
        // to the "message" action with operation status.
        $front->dispatch();
        $this->assertContains('User is registered', $response->getBody());
    }

    function testAnotherUseCase() {
        // ...
    }

}
protected\tests\application\main\models\UsersTests.php

require_once 'protected/application/main/models/Users.php';

class UsersTests extends PHPUnit_Framework_TestCase {

    function setUp() {
        // Prepare the database
        $db = Zend_Db_Table::getDefaultAdapter();
        $db->query('DELETE FROM user');
    }

    function testUserCreating() {
        $users = new Users();
        $user = $users->fetchNew();
        $user->name = 'joe';
        $user->password = md5('secret');
        $user->save();
        $addedUser = $users->find($user->id)->current();
        $this->assertTrue(is_object($addedUser));
        $this->assertEquals('joe', $addedUser->name);
    }

}

When you want to test your application and make sure that everything is OK just run AllTests.php using PHP CLI.

Here is a sample output of the "AllTests.php" file when all tests success:

PHPUnit 3.0.3 by Sebastian Bergmann.
 
...
 
Time: 00:00
 
 
OK (3 tests)

If some tests fail PHPUnit will display the message for each assertion that fails as follows:

PHPUnit 3.0.3 by Sebastian Bergmann.
 
..F
 
Time: 00:00
 
There was 1 failure:
 
1) testUserCreating(UsersTests)
Failed asserting that <string:joe> is equal to <string:kate>.
C:\htdocs\TestApp\protected\tests\application\main\models\UsersTests.php:21
C:\htdocs\TestApp\protected\tests\AllTests.php:23
C:\htdocs\TestApp\protected\tests\AllTests.php:35
 
FAILURES!
Tests: 3, Failures: 1.

You have good point, Thanks


You have good point, Thanks for the example.

problem with redirector


Hi, great article.
I have problem when I have redirections in controller:
$this->_helper->redirector('index', 'index', 'default');
i get Cannot modify header information - headers already sent

Re: problem with redirector


It is general error that indicates that some data was already sent to the user by echo call or some other method.

You can try to turn the output buffering on or to review the controller code and plugins and check whether the code outputs something or not.

Re: problem with redirector


Yes, but I get this message ("headers sent") because PHPUnit send test results to browser and now in the one of test functions it try to redirect, but ... headers already sent.
I don't want to check if controller code is run by test or by user and then redirect or not :D but I dont see another solution

Hi Alex,


Hi Alex,
good high level summary of MVC in the context of testing. I think it's important to identify how testing a model is different from testing a controller or a view. Oftentimes, with a model, writing tests (for example unit tests) can feel more like integration tests and very quickly, you're testing configuration and Zend_Db itself rather than the code you've written. This leads me to think that the concept of a Repository (from a Domain-Driven approach) makes a lot of sense as an intermediary between the Models and the Controller. Unfortunately, the Zend Framework doesn't have a place for this kind of architecture.

Selenium + PHPUnit is a great combination, however I'm still struggling to find a way of unifying my testing in a more TDD fashion, especially since Selenium seems to be a bit of an afterthought in it's testing approach.

I agree that automatic test scripts that are use-case driven should be devised for testing the controller, however with AJAX, I feel like another view that is not UI driven, but use-case driven is the best approach (something like FITness testing), but it might just add to the overhead of a project.

Thank publishing a concrete example of using PHPUnit with the Zend Framework's Controller architecture. I'm looking forward to working through your code samples!

Best Regards,
-
Jon Lebensold

Solving headers sent issue


As this is quite a popular reference for Controller testing in the Zend Framework, thought I would share how to avoid the headers already sent exception.

On the response object simply set

$response->headersSentThrowsException = false;

Regards,

Jamie Learmonth

another way to solve PHPUnit sending headers


In the spirit of the previous reply, here's another solution.

I'm doing isolated Learning Tests of the Zend_Auth API, which by default stores Auth responses in a session. I'm not bootstrapping a controller, much less dealing with a response object, and it doesn't seem to make sense to invoke several extra objects to get around the problem of PHPUnit sending headers (i.e. printing test results to the screen), which causes this sort of error when running the test:


1) testTryToCreateAndDestroySession_namespaceObject(LearningTest)
Zend_Session_Exception: Session must be started before any output has been sent to the browser; output started in /usr/local/lib/php/PHPUnit/Util/Printer.php/145

The workaround I've found is to call Zend_Session::start() *above* the declaration of the test class that extends PHPUnit_Framework_TestCase.

So the top of my file looks like this:


require_once 'PHPUnit/Framework.php';
require_once 'Zend/Loader.php';
$loader = new Zend_Loader;
$loader->registerAutoload();

Zend_Session::start();

class LearningTest extends PHPUnit_Framework_TestCase
{ // ...

Hope this is helpful to someone!

hard coded URL


I don't like that the URL has been hardcoded

$request = new Zend_Controller_Request_Http('http://localhost/main/user/register.do');

is it posible for the ZF to "guess" it? I mean, something like

${0} = array('module'=>'main', 'controller'=>'user', 'action'=>'registerDo');
$request = new Zend_Controller_Request_Http(${0});

PHPUnit and Zend_Session


Hi Alex,

I was playing with PHPUnit and have encoutered some problems when testing a controller which uses Zend_Session.
When I run the test I get the following exception :

"Zend_Session_Exception: Session must be started before any output has been sent to the browser; output started in"

Starting a new session using new Zend_Session_Namespace() seams to resolve the problem however I then get another exception when the program executes a _redirect() :

"Zend_Controller_Response_Exception: Cannot send headers; headers already sent in"

Have you experienced the same behavior ? Any help would be very appreciated.

Best regards,
sdu

Greetings


You have a good point there!!!

Indeed. This post was very


Indeed. This post was very long but extremely well written and enlightening Daniel Horace.