Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use a different email config during unit testing in CakePHP

I'm sending an email using the CakeEmail class in an action of one of my controllers. I have a unit test for this controller that was working fine before adding the email code. After adding the email I get this error:

SocketException: Could not send email.

This is down to the fact that I don't have any way of sending emails off my local machine.

So I thought that maybe a good idea would be to have two different configuration options inside the EmailConfig class in Config/email.php (Similar to how the database config file works). The default using the Mail transport, and a test using the Debug transport. The issue with this is that, unlike the database config, Cake doesn't automatically switch between the two during testing.

The only thing I have thought of is to add a constructor to the EmailConfig class and test if we're unit testing, but I'm not sure what the check should be.

Something along the lines of this:

class EmailConfig {

    public $default = array(
        'transport' => 'Mail'
    );

    public $test = array(
        'transport' => 'Debug'
    );

    public function __construct() {
        if ($isUnitTesting) {
            $this->default = $this->test;
        }
    }

}

Would my way suggested above be a good idea? If not, what other ways can I use a different transport for the email during unit testing?


Update - 4/10/2012

I think I was going about this the wrong way. Looking at this answer it seems that even the $default config isn't loaded by default, you have to specify it by calling the CakeEmail::config() method or give it in the constructor. So I think this leaves me two options now:

  1. In the controller check if we're unit testing (somehow?) and then use the 'test' config.
  2. Set up my computer to be able to send emails.

I'd rather do the first but unsure how this can be done without bloating the controller action with checks if we're unit testing, it seems wrong to do this.

like image 407
Josh Avatar asked Oct 07 '22 11:10

Josh


1 Answers

The easiest way is to probably switch to the DebugTransport while testing. Part of testing is that you need to design your program to be testable. In fact, there are a few functions here and there throughout Cake designed to do just that. For your app, let's assume you send an email when a user registers:

App::uses('CakeEmail', 'Network/Email');
App::uses('AppController', 'Controller');

class UsersController extends AppController {

  public function register() {
    //registration logic
    $email = new CakeEmail();
    $email->from(array('[email protected]' => 'Site'));
    $email->to('[email protected]');
    $email->subject('Registered');
    $email->send('Thanks for registering!');
  }

}

This looks harmless, but you cannot mock CakeEmail because it does not allow for dependency injection, which is necessary when testing. Instead, the CakeEmail class should be instantiated in a way that allows us to change it later. For example:

App::uses('CakeEmail', 'Network/Email');
App::uses('AppController', 'Controller');

class UsersController extends AppController {

  public function register() {
    //registration logic
    $email = $this->_getEmailer();
    $email->from(array('[email protected]' => 'Site'));
    $email->to('[email protected]');
    $email->subject('Registered');
    $email->send('Thanks for registering!');
  }

  public function _getEmailer() {
    return new CakeEmail();
  }

}

Because we added a little helper function, we can now test it (by mocking the helper function).

App::uses('CakeEmail', 'Network/Email');
App::uses('UsersController', 'Controller');

class UsersControllerTest extends ControllerTestCase {

  public function testRegister() {
    $controller = $this->generate('Users', array(
      'methods' => array(
        '_getEmailer'
      )
    ));
    $emailer = new CakeEmail();
    $emailer->transport('Debug');
    $controller
      ->expects($this->any())
      ->method('_getEmailer')
      ->will($this->returnValue($emailer));
  }

}

This test creates a mock object for our controller and tells it to return our newly created $emailer object when the _getEmailer method is called. Since $emailer has set the transport to 'Debug', it's safe for testing.

Of course, since now we're deciding what email object the method returns, mocking the CakeEmail object and expecting certain returns becomes trivial.

like image 111
jeremyharris Avatar answered Oct 10 '22 01:10

jeremyharris