CodeIgniter 4: Writing a unit test for sending emails

CodeIgniter 4: Writing a unit test for sending emails

Published at: 01/08/2023
Updated at: 01/08/2023
Categories
Tags

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.

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!

Leave a comment

All comments will be subject to approval after being sent. They might be published after several hours.

You can just use a random nickname, it is usefull to allow me to at least answer your comments. And if you choose to submit your email, you can receive a notification whenever I reply to your comment.

*
  • 2023-07-01 11:43:07

    Author: eartahhj

    @Atiab Jobayer I am glad it helped you!

  • 2023-07-01 01:28:35

    Author: Atiab Jobayer

    It was super helpful. Thanks a lot!