LogoPhly, boy, phly
the weblog and site of Matthew Weier O'Phinney

Monday, June 30. 2008

Testing Zend Framework MVC Applications

Since I originally started hacking on the Zend Framework MVC in the fall of 2006, I've been touting the fact that you can test ZF MVC projects by utilizing the Request and Response objects; indeed, this is what I actually did to test the Front Controller and Dispatcher. However, until recently, there was never an easy way to do so in your userland projects; the default request and response objects make it difficult to easily and quickly setup tests, and the methods introduced into the front controller to make it testable are largely undocumented.

So, one of my ongoing projects the past few months has been to create an infrastructure for functional testing of ZF projects using PHPUnit. This past weekend, I made the final commits that make this functionality feature complete.

The new functionality provides several facets:

  • Stub test case classes for the HTTP versions of our Request and Response objects, containing methods for setting up the request environment (including setting GET, POST, and COOKIE parameters, HTTP request headers, etc).
  • Zend_Dom_Query, a class for using CSS selectors (and XPath) to query (X)HTML and XML documents.
  • PHPUnit constraints that consume Zend_Dom_Query and the Response object to make their comparisons.
  • A specialized PHPUnit test case that contains functionality for bootstrapping an MVC application, dispatching requests, and a variety of assertions that utilize the above constraints and objects.

What might you want to test?

  • HTTP response codes
  • Whether or not the action resulted in a redirect, and where it redirected to
  • Whether or not certain DOM artifacts are present (particularly helpful for ensuring that the DOM structure is correct for JS actions)
  • Presence of specific HTTP response headers and/or their content
  • What module, controller, and/or action was used in the last iteration of the dispatch loop
  • What route was selected

The aim is to make testing your controllers trivial and fun. Let's look at an example:


class UserControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
    public function setUp()
    {
        $this->bootstrap = array($this, 'appBootstrap');
        parent::setUp();
    }

    public function appBootstrap()
    {
        $this->frontController->registerPlugin(
            new Bugapp_Plugin_Initialize('test')
        );
    }

    public function testCallingControllerWithoutActionShouldPullFromIndexAction()
    {
        $this->dispatch('/user');
        $this->assertResponseCode(200);
        $this->assertController('user');
        $this->assertAction('index');
    }

    public function testIndexActionShouldContainLoginForm()
    {
        $this->dispatch('/user');
        $this->assertResponseCode(200);
        $this->assertSelect('form#login');
    }

    public function testValidLoginShouldInitializeAuthSessionAndRedirectToProfilePage()
    {
        $this->request
             ->setMethod('POST')
             ->setPost(array(
                 'username' => 'foobar',
                 'password' => 'foobar'
             ));
        $this->dispatch('/user/login');
        $this->assertTrue(Zend_Auth::getInstance()->hasIdentity());
        $this->assertRedirectTo('/user/view');
    }
}
 

You'll note that the setUp() method assigns a callback to the $bootstrap property. This allows the test case to call that callback to bootstrap the application; alternately, you can specify the path to a file to include that would do your bootstrapping. In the example above, I actually simply add a single "initialization" plugin to the front controller that takes care of bootstrapping my application (via the routeStartup() hook).

I then have a few test cases. The first checks to ensure that the default action is called when no action is provided. The second checks to ensure that the login form is present on that page (by using a CSS selector to find a form with the id of 'login'). The third checks to see if I get a valid authentication session when logging in with good credentials, and that I get redirected to the appropriate location.

This is, of course, just the tip of the iceberg; I've created a couple dozen other assertions as well.

You can preview the functionality in the Zend Framework standard incubator; look for Zend_Test_PHPUnit_ControllerTestCase in there, as well as the Zend_Test documentation in the documentation tree (in human-readable DocBook XML).

For those of you who decide to start playing with this, I'd love any feedback I can get. The best place to do so, however, is on the fw-mvc mailing list; instructions are on the ZF wiki.

Posted by Matthew Weier O'Phinney in PHP at 12:00 | Comments (34) | Trackbacks (2)
Defined tags for this entry: best practices, mvc, php, zend framework
Related entries by tags:
Pastebin app updates
ZendCon08 Wrapup
git-svn Tip: don't use core.autocrlf
Setting up your Zend_Test test suites
Pastebin app and conference updates

Trackbacks
Trackback specific URI for this entry

Testing Zend Framework MVC Applications - phly, boy, phly
Testing Zend Framework MVC Applications - phly, boy, phly
Weblog: roScripts - Webmaster resources and websites
Tracked: Jul 01, 11:14
Testing models and controllers with Zend Framework: fake HTTP calls
Following last month's article by Ian, here's some thoughts on how to test a Zend Framework application. One of the unit testing best practices suggests to break dependencies, so you can test each component separately. The first problem that arises
Weblog: Ibuildings Blog
Tracked: Aug 29, 09:20

Comments
Display comments as (Linear | Threaded)

This looks awesome. I've been waiting for some really solid testing classes for the controller level and this looks like it'll be a perfect fit!
#1 Justin Hendrickson on 2008-06-30 12:11 (Reply)
Whoa!

I've been struggling since the beginning with unit testing my ZF MVC applications and this looks promising enough to take a deep dive and see how it can help me perform some TDD.

I didn't notice you worked on this project Matthew, thanks!
#2 Taco Jung on 2008-06-30 14:50 (Reply)
This looks really cool! In addition to the new testing facilities, Zend_Dom_Query sounds like it will be really helpful to me in another project I'm working on.

I hate to split hairs, but you said, "Mock test case classes for the HTTP versions of our Request and Response objects..." I think you meant stubs, not mocks:

http://martinfowler.com/articles/mocksArentStubs.html
http://www.phpunit.de/pocket_guide/3.0/en/mock-objects.html

If, indeed, you really meant mocks, then feel free to put me in my place :-)
#3 Bradley Holt (Link) on 2008-06-30 15:10 (Reply)
Indeed, I meant stubs, and I've updated it to reflect that. Thanks for the wrist slap!
#3.1 Matthew Weier O'Phinney (Link) on 2008-06-30 15:30 (Reply)
lol, no problem :-)
#3.1.1 Bradley Holt (Link) on 2008-06-30 15:34 (Reply)
Smells so good :-)
#4 JulienPauli (Link) on 2008-06-30 16:55 (Reply)
Looks quite good. This is just in time as I had actually been looking into ways to test ZF apps =)
#5 Jani Hartikainen (Link) on 2008-07-01 00:35 (Reply)
Perfect timing! Thanks Matt :-)
#6 Federico on 2008-07-01 04:47 (Reply)
Great! Have been looking forward to this!

Thanks!
#7 Anders Fredriksson (Link) on 2008-07-02 07:50 (Reply)
This looks cool!
but ...

I test this in 1.5.2. But lots of error happen.


Need I update to 1.6 ?

in 1.5.2, How can I test Controller ?


My Error:

class Rollenc_IndexControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
public function setUp()
{
$this->bootstrap = array($this, 'appBootstrap');
parent::setUp();
}

public function appBootstrap()
{
}


public function testIndexAction()
{
$this->dispatch('/');
$this->assertResponseCode(200);
$this->assertController('user');
$this->assertAction('index');
}
}

Zend_Controller_Exception: No default module defined for this application
/usr/share/php/Zend/Controller/Dispatcher/Standard.php:211
/usr/share/php/Zend/Controller/Dispatcher/Standard.php:245
/usr/share/php/Zend/Controller/Front.php:914
/var/www/rollenc/tests/Zend/Test/PHPUnit/ControllerTestCase.php:158
/var/www/rollenc/tests/Rollenc/IndexControllerTest.php:18
/var/www/rollenc/tests/Rollenc/AllTests.php:44
/var/www/rollenc/tests/Rollenc/AllTests.php:60


What's the wrong?
#8 rollenc (Link) on 2008-07-02 08:44 (Reply)
Your bootstrap needs to minimally set the controller directory -- do a call to $this->frontController->addControllerDirectory(...) in your appBootstrap() method. I didn't in my example, as my Initialization plugin does that sort of thing for me.
#8.1 Matthew Weier O'Phinney (Link) on 2008-07-02 08:57 (Reply)
I apologize in advance for my n00bness, but can you post your Bugapp_Plugin_Initialize plugin for reference?
#8.1.1 jsnod on 2008-08-14 18:59 (Reply)
Thanks for your this quick reply.
It is right for me.

Another problem:

I found there is class Zend_Session and other classes in Zend Framework standard incubator, Their name is as the same as ZF core.

Which is needed ?
If i include incubator class first, I will got nothing when I insert data to DB with mysqli adapter
#9 rollenc on 2008-07-02 09:44 (Reply)
The Zend_Session included in the incubator has some modifications that make it possible to test against it, though it is 100% BC with the version in trunk.

I myself have tested an app that included DB operations, and all worked correctly; however I was using the SQLite adapter. I see that there are local versions of the PDO adapter in the incubator, and perhaps this is causing the issue you are seeing.

This component will be ready for trunk shortly, and when it is, you should not see such issues.
#9.1 Matthew Weier O'Phinney (Link) on 2008-07-02 10:02 (Reply)
I see.

Zend_Db_Table_Abstract is changed in the incubator.
it need a preInsert notify, otherwise, return false before insert.

All the things are good after replacing Zend_Db_Table_Abstract file into ZF core.
#9.1.1 rollenc (Link) on 2008-07-03 04:47 (Reply)
There's any way to test, what objects the controllers returned, not just the HTML?

If i got an project controller, my index action will list my (5) projects.
Theres anyway to test like this:
$this->dispatch('/project');
$this->assert(5,$this->response->get('projects'));

Regards
#10 Rafael Mueller (Link) on 2008-08-08 15:41 (Reply)
How are your projects listed in the page? I'm assuming that you're generating HTML, and that there's a list. If so, you can use assertQueryContentCount() to perform such tasks: $this->assertQueryContentCount('#someId li', 5)
#10.1 Matthew Weier O'Phinney (Link) on 2008-08-08 15:47 (Reply)
Actually its just a hypothetical case.

I've been using TDD since 2005, in Java and Ruby we can test our controllers without a view.

I think thats a better way, since my controllers tests don't need to know nothing about my view.

My controller test must assure theres 5 projects, how they'll be rendered, its a view problem.

Thanks for your fast reply

Best regards,
#10.1.1 Rafael Mueller (Link) on 2008-08-08 15:56 (Reply)
nice, i got it :-D

I just created a class, which extends Zend_View, overrided the "__set" method and now i store the objects on this new fake view class.

On my TestInitializer (extends Zend_Controller_Plugin_Abstract), __construct method i added few lines too:

$view = new TestView();
$this->_front->setParam('view',$view);
$viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer($view);
Zend_Controller_Action_HelperBroker::addHelper($viewRenderer);

So i can test my controllers without rendering the view, like this:
$view = $this->_frontController->getParam('view');
$this->assertEquals(5, count($view->get('projects')));

Now i'll just refactor a little to make it more easy to read the tests
#10.1.1.1 Rafael Mueller (Link) on 2008-08-11 09:54 (Reply)
I found something wrong with Zend_Test, isn't is right. I don't know, I think that I must tell you that in setMethod('GET') and setQuery then dispatch($url), something wrong happened.
array $_GET is empty.
I send a mail to you, I maybe has resolved it and hoped that it could help us more happiness
#11 jfcat (Link) on 2008-08-26 21:56 (Reply)
Yes, received your mail. This was reported on the issue tracker yesterday, and I was finally able to verify it this morning. It is definitely an issue with how $_GET is populated from the query string, and your patch should help resolve the issue.
#11.1 Matthew Weier O'Phinney (Link) on 2008-08-26 22:25 (Reply)
A couple of questions:

1. How is it possible in the Zend framework to unit test the functions in my controller (that extends the ZendControllerAction class) without having to go through bootstrapping the zend framework etc. Currently we're overriding the constructor and using a parameter to indicate if its being run as part of a test but this isn't really elegant and sustainable.

2. Obviously using the Zend_Test_PHPUnit_ControllerTestCase class is the standard answer to question 1, however we're using the PHPUnit_Extensions_Story_TestCase for some BDD goodness which rules out using this and this class still doesn't allow the functions in the controller to be unit tested in isolation from the framework.

Any help is most welcome.

Rgds

rob
#12 Rob Hathaway on 2008-09-09 10:13 (Reply)
Zend_Controller_Action takes three parameters: a request object, a response object, and an array of invocation parameters (usually these are passed in from the front controller). So, to unit test without using Zend_Test_PHPUnit, simply make sure you instantiate your controller with the proper objects.

You'll also have a few other concerns: making sure the HelperBroker is setup correctly (i.e., the expected helpers are either present in the broker prior to instantiating the controller, and that the expected helper paths are set). The key one to ensure is present is the ViewRenderer. But that's all there is to it.
#12.1 Matthew Weier O'Phinney (Link) on 2008-09-09 10:55 (Reply)
Thanks for the quick response :-)
I'm assuming that there is no way to test the code in my controllers without running some part of the Zend framework?

If it requires setting up the framework it's integration testing...not unit testing which is what I'm interested in when doing TDD.
#12.1.1 Rob Hathaway on 2008-09-09 11:49 (Reply)
You're *already* running part of Zend Framework if you're using Zend_Controller_Action. I'm really not sure what you're driving at.

As for unit versus functional testing, the approach you describe -- of instantiating the controller and testing individual methods -- is definitely unit testing. The other classes I mention -- the helper broker, the request/response objects, view object, etc. -- are part of your testing environment -- think of them as the scaffolding for your test. They can certainly be mocked or stubbed, but they are still necessary in order to execute the test.
#12.1.1.1 Matthew Weier O'Phinney (Link) on 2008-09-09 12:00 (Reply)
would you please tell me how can i run a test ?!
should i point a url in browser ?
or it should be run in zend studio ?!
how can i run the file ?!
#13 Ramin (Link) on 2008-09-11 05:54 (Reply)
You'll need PHPUnit, and once you have that, simply 'phpunit UserControllerTest' should run the test (on the CLI). But you'll need to know a bit about PHPUnit to make sure your infrastructure is setup correctly. I'll be blogging on that in coming weeks.
#13.1 Matthew Weier O'Phinney (Link) on 2008-09-11 06:55 (Reply)
Thx.So there is no way like the one in zend studio which you click the run button and go for run as PHPUnit text ?!

There is no other way of doing so without using CLI ?!
#13.2 ramin on 2008-09-11 08:29 (Reply)
Yes, of course you can run these test cases within Zend Studio -- they're valid PHPUnit test cases.

I just usually run them from the CLI. This allows running on a cronjob, with a CI server, or as a post-commit hook with your svn repository.
#13.2.1 Matthew Weier O'Phinney (Link) on 2008-09-11 08:43 (Reply)
Hi,

I'm making use of a custom inherited repsonse and request by

request = new Herod_Controller_Request_Http($this->config);
$response = new Herod_Controller_Response_Http($this->config);

$this->frontController->setRequest($request);
$this->frontController->setResponse($response);

I tried to also do this in my appBootstrap within my ControllerTest-class.

By running the test I get bugged by
"Fatal error: Call to undefined method Zend_Controller_Response_HttpTestCase::addResponseJSCall() in /Users/marcofrank/workspace_zse_6_1/weblibs_own/hf/herod_1/library/Herod/Controller/Plugin.php on line 72"

The test never regards my custom response-class.
Can somebody explain my how to advice the test to take another class like Zend_Controller_Response_HttpTestCase?

I would like to inherit a custom version of that class that implements function that are contained in my regular abstraction of the response class.

thx and greetings!
marco
#14 Marco on 2008-09-21 04:32 (Reply)
Currently, custom request and response objects are not supported; the stub classes we created have additional functionality not in the standard interfaces. Our plan is to introduce "Testable" interfaces for both request and response objects in a coming release.
#14.1 Matthew Weier O'Phinney (Link) on 2008-09-21 12:38 (Reply)
First of all this test class is great - makes testing so much easier than it would have been without it.

That being said - I'm having problems bootstrapping my test cases. I define several constants in the bootstrap file which cause errors when setUp() is run several times (once for each test method). What is the intended way of bootstrapping the application so that the bootstrap isn't run again each time another test method is evaluated?

I dislike the idea of adding if(!defined('constant')) to each of the defines just because the tests have issues here.
#15 Jukkad on 2008-09-24 07:36 (Reply)
You may dislike conditional constants, but it's a really good mechanism. I do it on one line:

defined('CONSTANT_NAME') or define('CONSTANT_NAME', 'value');

which is somewhat perlish, but gets it across really well. It's good defensive programming when it comes to constants.

The bootstrap code is run for every test. This is to ensure that you have a clean environment for each and every test case. In PHPUnit 3.3, each test can actually be run in its own PHP process if desired (takes more time and memory), which would eliminate such warnings as you are seeing.
#15.1 Matthew Weier O'Phinney (Link) on 2008-09-24 09:36 (Reply)
Thanks for the answer, I suspected this might be the case.

Running each of the tests in its own process might be an option while the number of test cases is small, but as they crop up the performance hit may well be too heavy.

I like your perlish way of doing the ifndef, by they way, lot cleaner than the full fledged if! clause.
#15.1.1 Jukkad on 2008-09-25 04:05 (Reply)

Add Comment

Standard emoticons like :-) and ;-) are converted to images.
E-Mail addresses will not be displayed and will only be used for E-Mail notifications

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA

 
 
  • Home
  • Resume
  • Blog
  • Phly PEAR Channel
  • Contact Me
  • About this site

ZCE

Zend Education Advisory Board Member

Add to Technorati Favorites

Calendar

Back October '08
Mon Tue Wed Thu Fri Sat Sun
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31    

Quicksearch

Links

  • PHLY - PHp LibrarY
  • Paul M. Jones
  • Mike Naberezny
  • Shahar Evron
  • Planet PHP
  • Zend Where I now work
  • Garden.org Where I once worked

Archives

October 2008
September 2008
August 2008
Recent...
Older...

Categories

XML Linux
XML Personal
XML Aikido
XML Family
XML Programming
XML Dojo
XML Perl
XML PHP

All categories

Syndicate This Blog

XML RSS 0.91 feed
XML RSS 1.0 feed
XML RSS 2.0 feed
ATOM/XML ATOM 0.3 feed
ATOM/XML ATOM 1.0 feed
XML RSS 2.0 Comments

Show tagged entries

xml best practices
xml books
xml conferences
xml dojo
xml dpc08
xml file_fortune
xml linux
xml mvc
xml oop
xml pear
xml personal
xml php
xml phpworks08
xml programming
xml ubuntu
xml webinar
xml zendcon
xml zendcon08
xml zend framework
© 2004 - present, Matthew Weier O'Phinney
matthew-web <at> weierophinney.net