Building “the blog tutorial”… the TDD way (part 2 – controller testing)

CakePHP 2.2

We have prepped our app with enough testing, to be ready to move on to write out the rest of the code…
There was a good reason why we have started our TDD with the model layer. By encapsulating the business logic into our models we were able to test our application quite well, without having to write any controller code.

Now, feeling a lot more confident, let’s go ahead and create app/Controller/PostsController.php:

<?php
class PostsController extends AppController {
   
    public function index() {
        $this->set('posts', $this->Post->getAllPosts());
    }
}

We already know that our getAllPosts() method is working well, because it’s been tested in the model. Therefore our controller test will simply re-enforce that knowledge and complete the cycle by making sure our expectation of variables set() for the view matches the return. One simple example of where this could be an issue is a heavy and complex Post model, which has a ton of methods.
It is not that far-fetched to write:

$this->set('posts', $this->Post->getEachPost());

Perhaps, we’ve inherited the app from another developer or simply weren’t sure what getEachPost() method does, or made the wrong presumption that this is the method we need to be using for the index action. Thus, creating a simple test case can secure out intent and expectation on the controller level.

app/Test/Case/Controller/PostsControllerTest.php:

<?php
class PostsControllerTest extends ControllerTestCase {
    public $fixtures = array('app.post');
       
    public function testIndex() {
      $this->testAction('/posts/index');
      $this->assertInternalType('array', $this->vars['posts']);
      $expected = array(
          array(
              'id' => 2,
              'title' => 'A title once again'
          )
      );
      $result = Hash::extract($this->vars['posts'], '{n}.Post[id=2]');
      $this->assertEquals($expected, $result);
     
    }

As you see we’ve used the same fixture as we did in part 1, while testing our Post Model. Here, we have leveraged the index() action of the Posts Controller and made sure that our record with ID = 2, matches our expected return from the Post Model and sets the correct output to be used in the view. You’ll notice the use of $this->vars, which is simply a CakePHP way of storing the view variables within the testing environment.
The two assertions we have tested for:
1. That we have some array of variables ready for display (i.e. our Posts).
2. That a record with ID = 2, is the Post.id and Post.title, which, as we know, ultimately comes from our Post Fixture.
Once again, we have now bridged our test to cover everything from the model layer to controller to the setting of the view variables.

Our view() action and test are not going to be much different, and since we are well familiar with what our output and expectations should be, let’s add the missing bits to move on with our application development.

First we’ll add the new action to our Posts Controller:

public function view($id = null) {
      $this->set('post', $this->Post->getSinglePost($id));
    }

And the test case:

public function testView() {
      $this->testAction('/posts/view/3');
      $this->assertInternalType('array', $this->vars['post']);
      $expected = array(
          'Post' => array(
            'id' => '3',
            'title' => 'Title strikes back',
            'body' => 'This is really exciting! Not.',
            'created' => '2012-07-04 10:43:23',
            'updated' => '2012-07-04 10:45:31'
          )
      );
      $this->assertEquals($expected, $this->vars['post']);
    }

By now you should be armed with enough knowledge to figure out this simple addition to our app on your own.

Now we can get into something a little more interesting and take a look at how we would use Mock Objects to test our add() action.
This is a nice show-off of the simplicity of such a powerful feature and how nicely it is integrated in CakePHP. Sometimes we don’t need or don’t want to have a full-blown app written just to test bits of logic (as the case might be with the simple add() method in our little app). Therefore cake will generate “fake” objects (i.e. Mock Objects) and leverage some, but not all, of the existing code to do our testing.

Let’s beef up the Posts Controller with the new add() action:

public function add() {
        if ($this->request->is('post')) {
            if ($this->Post->addPost($this->request->data)) {
                $this->Session->setFlash('Your post has been saved.');
                return $this->redirect(array('action' => 'index'));
            } else {
                $this->Session->setFlash('Unable to add your post.');
            }
        }
    }

… and let’s take a look at the test case:

public function testAddViaMock() {
      $postData = array(
          'Post' => array(
            'title' => 'New Post Title',
            'body' => 'TDD FTW!'
          )
      );
           
      $this->testAction('/posts/add', array(
          'data' => $postData,
          'method' => 'post'
          )
      );
     
      $this->assertContains('http://app/posts', $this->headers['Location']);
      $this->assertEquals(4, $this->controller->Post->find('count'));    
    }
   
public function testAddViaMockWithBadData() {
      $postBadData = array(
          'Post' => array(
            'title' => '',
            'body' => 'TDD FTW!'
          )
      );
     
      $this->testAction('/posts/add', array(
          'data' => $postBadData,
          'method' => 'post'
          )
      );
           
      $this->assertTrue(!empty($this->controller->Post->validationErrors));
      $this->assertContains('This field cannot be left blank', $this->controller->Post->validationErrors['title']);
      $this->assertEquals(3, $this->controller->Post->find('count'));
      $this->assertTrue(empty($this->headers));
    }

As you see we’ve actually added two methods to test some assertions. One comes with “good” data and one with “bad” data (empty title). This helps us to keep things more granular and allows to test our if/else conditions all the way to saving of the data and model validation.

Behind the scenes cake will Mock a controller object for us, meaning it created a “fake” Posts Controller. Although in reality the framework still leverages the existing code, yet only minimal parts are required for testing. There is an option to fine-tune the creation of such Mock Objects by using the generate() method, but in this case we can get away by relying on the already created model, fixture and bits of the controller.
I mentioned before that models are easier to test and that it is always a good idea to keep the logic in the model, hopefully you can start to see why. Had we placed all the logic in the controller the testing would become a lot more complicated and at the same time not nearly as precise as what we have now.
It does take a little a practice to write good test cases. However, by employing the TDD way of thinking we are naturally pushed towards making better decisions about logic separation and thereby necessity forces us to improve the overall architecture.

I will let you browse the code and take a look at the assertions, there really isn’t anything new there, that we have not covered yet. Just to summarize:
1. We test user input with good data, and make sure our if/else and redirection works as expected. Hint: $this->assertContains(‘http://app/posts’, $this->headers[‘Location’]);. (Yep, the post was saved, therefore we got redirected back to “index”).
2. We make sure our new post is indeed saved as expected: $this->assertEquals(4, $this->controller->Post->find(‘count’));
3. In the second method we ensure that validation will kick-in, if a user forgets to add a post title.
4. And double-check that no data will be stored in this case.

This post is already getting rather long, so I will not bore you anymore by showing the edit() action and the test case for it. In reality it would be nearly identical to the add() action, and since we’ve seen these methods tested on the model layer it shouldn’t take you long to build up a test case on your own.

Related Posts

%d bloggers like this: