A unit test for testing that emails can be sent within our application
Long story short: I am not very used to unit testing, and I wanted to learn something new. I decided to write a unit test to make sure I can send emails from my CodeIgniter 4 application.
Well, turns out that by default, the email service of CodeIgniter 4 will use a Mocked Class to do this, and therefore my test was always returning true for the send() method of the Email class.
Table of contents
Writing the test
To begin, this is the test I wanted to perform:
$email = service('email');
$email->setFrom(env('email.from'), env('email.fromName'));
$email->setTo(env('email.testRecipient'));
$email->setSubject('Email Test');
$email->setMessage('Testing that the email can be sent properly.');
$this->assertTrue($email->send()); // Why does this always return true?
In general the idea seems fine. But there was a problem. My test was always returning true, even if the email was not being sent.
Why does the test return true?
I am not yet an expert of unit testing, so I thought I was doing something wrong. Of course my expected result was to pass the test, but the email was not actually being sent. Moreover, I am still unexperienced with CodeIgniter 4. So, I decided to hunt down the issue, and I found the "culprit", so to say.
By default, CodeIgniter will perform tests using some predefined mock classes, that is a set of fake classes that should not actually perform definitive actions.
In the docs it says that this is made on purpose to prevent intrusive testing behavior (see here: Testing).
My test was always returning true because it was using the send() method of the mocked class, not the real Email class.
MockEmail Class
Indeed, the MockEmail class is as follows:
class MockEmail extends Email
{
/**
* Value to return from mocked send().
*
* @var bool
*/
public $returnValue = true;
public function send($autoClear = true)
{
if ($this->returnValue) {
$this->setArchiveValues();
if ($autoClear) {
$this->clear();
}
Events::trigger('email', $this->archive);
}
return $this->returnValue;
}
}
As you can see, the send() method does not really send the email. Or, at least, I needed to send an SMTP message and this method is not doing that.
You could probably just override the MockEmail class, but this didn't seem to be a real case scenario to me. I mean, I want to test that the application can actually send emails to my users, so I need to follow the same path, otherwise the test would not make much sense, would it?
If I have to test that I can receive the email, I don't want a mock send(), right?
Overriding the setUpMethods of the Test Class
In the docs, it is also said that you can remove 'mockEmail' from the Test class, within the $setUpMethods.
In the code for $setUpMethods there is this warning comment:
// CodeIgniter\Test\CIUnitTestCase.php
/**
* Methods to run during setUp.
*
* WARNING: Do not override unless you know exactly what you are doing.
* This property may be deprecated in the future.
*
* @var array of methods
*/
protected $setUpMethods = [
'resetFactories',
'mockCache',
'mockEmail',
'mockSession',
];
So going with that solution seems to be discouraged. But what could a "intrusive behavior" be in the testing phase? I am not sure. Maybe if you make tests to be available with public calls (get, post) there could be a problem. This is not my case.
Solution and working test
My solution is to bypass the mocked class, as follows.
<?php
// tests\app\Emails\SendEmailTest.php
namespace App\Emails;
use CodeIgniter\Test\CIUnitTestCase;
final class SendEmailTest extends CIUnitTestCase
{
private $email = null;
public function __construct()
{
parent::__construct();
// Removing the mockEmail from this test
$k = array_keys($this->setUpMethods, 'mockEmail')[0];
unset($this->setUpMethods[$k]);
}
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
return;
}
protected function setUp(): void
{
parent::setUp();
$this->email = service('email');
$this->email->setFrom(env('email.from'), env('email.fromName'));
$this->email->setTo(env('email.testRecipient'));
return;
}
final public function testCanSendEmails()
{
$this->email->setSubject('Email Test');
$this->email->setMessage('Testing that the email can be sent properly.');
$result = $this->email->send();
if (!$result) {
d($this->email->printDebugger());
}
$this->assertTrue($result);
}
}
Conclusions
The question is: am I correct to think that it is not a logical behavior and it should be improved, or is there a good reason for doing so, that I do not know of? Is there a better way to achieve this?
Let me know your opinions in the comments below! If you liked this article, please follow me on Facebook and Youtube!