Friday, 16 August 2013

Strategy of dealing with private methods in TDD for CakePHP

If you are writing, sooner or later you will run into question like 'how do I test private methods?' or 'What is the way to go about private methods in testing?'.

Let us explore two means of dealing with this issue. And I will use example in the context of CakePHP but I believe this applies to all PHP application and possibly other language as well.

An example

We have a model class called 'Post' which needs two methods named 'add' and 'edit'. They both send out email if a post is saved successfully, which means there will be a private method in the 'Post' class to handle this.

The first solution will be writing a private method in the test to mock up 'CakeEmail' and expects 'config' method is called on it with an array contains email configuration and data passed from the tests for 'edit' and 'add' methods and then 'send' method is expected to be called. It looks like this:
    public function testEdit()
    {
        ... // testing editing post and generate an $email
        $this->sendEmail($email);
    }
    
    public function testAdd()
    {
        ... // testing adding post and generate an $email
        $this->sendEmail($email);
    }
    
    private function sendEmail($email) {
        $this->Post->CakeEmail = $this->getMock('CakeEmail', array(
            'config',
            'send'
        ));

        $this->Post->CakeEmail->expects($this->once())
            ->method('config')
            ->with($email);

        $this->Post->CakeEmail->expects($this->once())
            ->method('send');
    }

This way we can clearly see that the code in 'Post' will be a private method in charge of sending email and 'edit' and 'add' both utilize this method to send email out.

The second solution is using 'Reflection' and 'runkit'. The idea is to use runkit to change 'sendEmail' method from 'private' to 'public' and in the tests for 'add' and 'edit' we mock up 'sendEmail' method so we can expect it is called. Then we write a test for 'sendEmail' mocking up 'CakeEmail' and using 'Reflection' to invoke 'sendEmail' in the test. This way we make sure the internal structure of 'Post' method is tested which means encapsulation is broken and any future change of the name of 'sendEmail' method will need change the test for it and involve it first. Thus, I don't recommend taking this approach.