Testing with CakePHP 1.2 – A preview

Published on and tagged with cakephp  testing  tutorial

In an earlier post I asked what’s missing for testing a CakePHP application. In the meantime I created a test suite which implements the missing features mentioned in that post. In this article I will introduce this new test suite. (please notice that I don’t know yet whether this test suite will become the official test suite)

Ok, let us start with the installation, which is simple (I assume you already installed SimpleTest):

  • Download the test suite
  • Copy the content of the “vendors” folder from the zip to your global vendors folder
  • Copy the content of the “app” folder to your app folder
  • Define the database connection(s) for testing in app/config/test-database.php

That’s it.

The reason the test suite uses its own database configuration file is that it is more flexible than using the $test configuration predefined in database.php.

While copying the files you may have noticed the new directory structure for the tests:

app
  tests
    components
    controllers
    fixtures
    groups
    helpers
    models

The folders “components”, “controllers”, “helpers” and “models” are for the tests of the respective classes. The “fixtures” folder is for fixtures and “groups” for group tests.

Now let us write some tests to see the test suite in action.

We start with a test for a User model with just one method: isLoginAllowed.

// app/tests/models/user_test.php
class UserTest extends CakeTestCase {
    var $User = null;
    var $fixtures = array('Users');
		
    function setUp() {
        $this->User = new User();
    }
		
    function testIsLoginAllowed() {
        $this->assertTrue($this->User->isLoginAllowed($this->dho['username'], $this->dho['password']));
        $this->assertFalse($this->User->isLoginAllowed($this->dho['username'], 'invalid'));
        $this->assertFalse($this->User->isLoginAllowed('invalid', $this->dho['password']));
    }
}		

As you see our test extends CakeTestCase, the base class for all test cases. We have also to follow the naming convention for test cases: same name as the object under test plus “Test”, e.g. UserTest, UsersControllerTest. To make our testing job a bit easier we use a fixture:

// app/tests/fixtures/users.php
class Users {
    var $columns = array('id', 'username', 'password');
    var $dho = array(1, 'dho', 'geheim');
}		

You probably know this fixture. It is the same I have used in the article about the Selenium test suite. This means you can reuse fixtures in your Selenium tests.

With that, we have written our first test, and now we can execute it. There exist different options how we can execute our test. We can use the command line:

// execute all tests
php bake2.php test myapp
// execute all model tests
php bake2.php test myapp models
// execute the tests for the User model
php bake2.php test myapp model User

or we can use the web interface (a simple wrapper for the command line):

// execute all tests
http://myapp.localhost/tests/all
// execute all model tests
http://myapp.localhost/tests/models
// execute the tests for the User model
http://myapp.localhost/tests/model/User

As we don’t have implemented anything we get as expected some failures. I omit the implementation of the User model, as it is not that interesting in this context.

Testing components and helpers is similar to testing models, so we can go directly to the testing of controllers. As it is rather difficult to unit test controllers due to all the dependencies a controller usually has, we will write functional tests.

In our case we will write tests for the “login” action of the “UsersController”. As this action can be called with both GET and POST requests, we have to write at least two tests. Let us start with the test for the GET request:

// app/tests/controllers/users_controller_test.php
class UsersControllerTest extends CakeTestCase {

    function testLogin() {
        $this->get('/users/login');
        $this->assertResponse(SUCCESS);
    }
}

This test is rather simple: we simulate a GET request and check whether the response was a SUCCESS. SUCCESS means there was no cake error or redirect.

More interesting are the tests for POST requests. We test two cases: a successful and a not successful login. If the login is successful, the user should be redirected to /projects and the user id must be stored in the session. In the other case a flash message should be shown without doing a redirect.

// app/tests/controllers/users_controller_test.php
class UsersControllerTest extends CakeTestCase {
    var $fixtures = array('Users');

    function testLogin() {
        $this->get('/users/login');
        $this->assertResponse(SUCCESS);
    }

    function testLoginValidUser() {	
        $this->post('/users/login', array('User' => array('username' => $this->dho['username'], 'password' => $this->dho['password'])));
        $this->assertResponse(REDIRECT);
        $this->assertRedirectedTo('/projects');
        $this->assertEqual($this->dho['id'], $this->session['User.id']);
    }

    function testLoginInvalidUser() {	
        $this->post('/users/login', array('User' => array('username' => 'invalid', 'password' => 'invalid')));
        $this->assertResponse(SUCCESS);
        $this->assertFlash('Invalid username/password');
    }
}		

With that we have written our first controller test. We can execute it in the same way as we executed the model test.

Last, but not least, let us group the two tests we have written up to now in a group test:

// app/tests/groups/user_tests.php
class UserTests extends CakeGroupTest
{
    function UserTests()
    {
        $this->addTestFile('controllers'.DS.'users_controller_test.php');
        $this->addTestFile('models'.DS.'user_test.php');
    }
}

Our group test has to extend “CakeGroupTest”, but we are free to name it in any way we like. To execute our group test we have to use:

php bake2.php test myapp group UserTests

or

http://myapp.localhost/tests/group/UserTests

Ok, that’s it.

Happy testing :)

59 comments baked

  • Irina Dumitrascu

    How we mocked our components (would qualify as a slightly-soiled hack :) )

    Another thing we needed for testing was to mock some components. We wanted to bypass the authentication and use a simple, fake authentication component that would pass as authenticated the user we wanted.

    In order to do that we:
    – hijacked cake’s dispatcher class, separating the fragment that initializes the controller and starts it in a function
    – in testsuite’s test_dispatcher class the function was overridden, and we:
    – added the FakeAuth component to the set of components of the controller
    – started the controller
    – modified the pointer to the Auth component to point to our FakeAuth object, that was created when the controller was started

    Please let me know if there is any less intrusive way to do this.

    http://www.geocities.com/irina2000d/dispatcher.patch.txt
    http://www.geocities.com/irina2000d/test_dispatcher.patch.txt

  • cakebaker

    @Richard: You have to instantiate the component and set it as property of your component, for example:

    public function setUp() {
        $this->myComponent = new MyComponent();
        $this->myComponent->Session = new SessionComponent();
    }
    

    @Irina: Thanks for your explanations! I will add the snippet to the code as it makes sense to show the errors.

    Regarding the post and get methods, do you think it would be better if the data have to be passed in the form “data[Model][field]”?

  • Noi @ Dream Production » Blog Archive » Lista.lu - de la start la private beta

    […] facut teste (adica, facem TDD – Test Driven Development), trecem pe cararea mai putin oficiala a modulului cu simpleTest pentru ca cel oficial si inca in beta are un caz pe care nu merge si nu reusim sa gasim cauza in […]

  • Richard Yumul

    Is there a way to make cake produce the same resulting HTML when run under the testsuite versus what cake produces when an action is hit thru the web server?

    I’d like to be able to capture the output. In other words, I’d like to be able to be able to capture the output when an action is executed from the testsuite, and that ideally should be the same HTML returned when the action is executed when hit thru the web server. Does that make sense?

    class FooTest extends CakeTestCase {
      function testBar() {
        $this->get('/foo/bar');
        // how do I get the HTML output now?
      }
    }
  • cakebaker

    @Richard: Well, you can replace the render() method in testable_controller.php with:

    function render($action = null, $layout = null, $file = null) {
        $this->viewContent = parent::render($action, $layout, $file);
    }

    In your test method you can then access the view content with:

    $this->controller->viewContent;
    

    Hope that helps!

  • Doug Kulak

    Is this still the best way to test CakePHP? I’m brand new to unit testing and want to be sure I’m off on the right track!

  • cakebaker

    @Doug: Thanks for your comment!

    No, this test suite is outdated, and so I recommend to use the official test suite which is bundled with CakePHP.

    Hope that helps, and happy testing :)

  • Doug Kulak

    @cakebaker: Thanks for the quick response! I’m relieved to hear that the CakePHP community is on top of things :)

  • Lista.lu – de la start la private beta | Dream Production

    […] teste (adica, facem TDD – Test Driven Development), trecem pe cararea mai putin oficiala a modulului cu simpleTest pentru ca cel oficial si inca in beta are un caz pe care nu merge si nu reusim sa gasim cauza in […]

Bake a comment




(for code please use <code>...</code> [no escaping necessary])

© daniel hofstetter. Licensed under a Creative Commons License