CodeIgniter 4: Creare uno unit test per inviare email

CodeIgniter 4: Creare uno unit test per inviare email

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

Uno unit test per testare che la nostra applicazione possa inviare email

Per farla breve: non sono molto abituato agli unit test e volevo imparare qualcosa di nuovo. Ho deciso di scrivere un test unitario per assicurarmi di poter inviare e-mail dalla mia applicazione su CodeIgniter 4.

Succede però che, per impostazione predefinita, CodeIgniter 4 utilizzerà una finta classe email per farlo, e quindi il mio test restituiva sempre true per il metodo send() della classe Email.

Scrivere il test

Per iniziare, questo è il test che volevo fare:

$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()); // Perché ritorna sempre true?

In generale l'idea sembra buona. Ma c'era un problema. Il mio test restituiva sempre true, anche se l'e-mail non veniva inviata.

Perché il test ritorna true?

Non sono ancora un esperto di unit test, quindi ho pensato che stavo facendo qualcosa di sbagliato. Ovviamente il risultato che mi aspettavo era di superare il test, ma l'e-mail non veniva effettivamente inviata. Inoltre, non ho ancora esperienza con CodeIgniter 4. Quindi, ho deciso di dare la caccia al problema e ho trovato il "colpevole", per così dire.

Di base, CodeIgniter eseguirà i test utilizzando alcune classi mock predefinite, ovvero un insieme di classi fittizie che non dovrebbero effettivamente eseguire azioni definitive.

Nella documentazione si dice che questo è fatto apposta per prevenire comportamenti di test intrusivi (vedi qui: Testing).

Il mio test restituiva sempre true perché veniva usato il metodo send() della classe mock, non la vera classe Email.

MockEmail Class

In effetti, ecco la classe MockEmail:

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;
    }
}

Come possiamo vedere, il metodo send() non invia realmente l'e-mail. O, almeno, avevo bisogno di inviare un messaggio SMTP e questo metodo non lo fa.

Probabilmente potremmo semplicemente sovrascrivere la classe MockEmail, ma questo non mi è sembrato uno scenario reale. Voglio dire, voglio testare che l'applicazione possa effettivamente inviare email ai miei utenti, quindi devo seguire lo stesso percorso, altrimenti il test non avrebbe molto senso, vero?

Se devo verificare di poter ricevere l'e-mail, non voglio un finto send(), giusto?

Sovrascrivere setUpMethods della classe Test

Nella documentazione, si dice anche che possiamo rimuovere "mockEmail" dalla classe Test, all'interno di $setUpMethods.

Nel codice per $setUpMethods c'è questo commento di avviso:

// 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',
    ];

Quindi andare con quella soluzione sembra essere scoraggiato. Ma quale potrebbe essere un "comportamento intrusivo" in fase di test? Non sono sicuro. Magari facendo dei test che siano raggiungibili con chiamate pubbliche (get, post) potrebbe esserci un problema. Questo non è il mio caso.

Soluzione e test funzionante

La mia soluzione è di bypassare la classe mock, così:

<?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);
    }
}

Conclusioni

La domanda è: ho ragione a pensare che non è un comportamento logico e dovrebbe essere migliorato, o c'è una buona ragione per farlo, che non conosco? C'è un modo migliore per raggiungere questo obiettivo?

Fatemi sapere le vostre opinioni nei commenti qui sotto! Se ti è piaciuto questo articolo, seguimi su Facebook e Youtube!

Leave a comment

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

Fields marked with * are mandatory. Email is optional and will not be published in any case.

No comments have been written so far on this article. Be the first to share your thoughts!

*