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:
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With