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.
Tabella dei contenuti
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!