Un test unitario para probar que se pueden enviar emails dentro de nuestra aplicación
Resumiendo: no estoy muy acostumbrado a las pruebas unitarias, y quería aprender algo nuevo. Decidí escribir un test unitario para comprobar que puedo enviar emails desde mi aplicación CodeIgniter 4 .
Bueno, resulta que por defecto, el servicio de correo electrónico de CodeIgniter 4 utilizará una clase Mocked para hacer esto, y por lo tanto mi prueba siempre devolvía true para el método send() de la clase Email.
Tabla de contenidos
Escribiendo el test
Para empezar, este es el test que quería realizar:
$email = service('email'); $email->setFrom(env('email.from'), env('email.fromName')); $email->setTo(env('email.testRecipient')); $email->setSubject('Email Test'); $email->setMessage('Probando que el email puede ser enviado correctamente.'); $this->assertTrue($email->send()); // ¿Por qué esto siempre devuelve true?
En general la idea parece buena. Pero había un problema. Mi test siempre devolvía true, incluso si el email no se enviaba.
¿Por qué la prueba devuelve true?
Todavía no soy un experto en pruebas unitarias, así que pensé que estaba haciendo algo mal. Por supuesto, mi resultado esperado era pasar la prueba, pero el correo electrónico no se enviaba. Además, todavía no tengo experiencia con CodeIgniter 4. Así que decidí buscar el problema y encontré al "culpable", por así decirlo.
Por defecto, CodeIgniter realizará pruebas utilizando algunas clases simuladas predefinidas, es decir, un conjunto de clases falsas que en realidad no deberían realizar acciones definitivas.
En la documentación se dice que esto se hace a propósito para evitar un comportamiento intrusivo en las pruebas (ver aquí: Pruebas).
Mi test siempre devolvía true porque estaba usando el método send() de la clase mock, no de la clase Email real.
Clase MockEmail
Efectivamente, la clase MockEmail es la siguiente:
class MockEmail extends Email { /** * Valor a devolver de mocked send().
* @var bool */ public $valorDeVuelta = true; public function send($autoClear = true) { if ($this->valorDeVuelta) { $this->setArchiveValues(); if ($autoClear) { $this->clear(); } Events::trigger('email', $this->archive); } return $this->valorDeVuelta; } }
Como puede ver, el método send() no envía realmente el correo electrónico. O, al menos, yo necesitaba enviar un mensaje SMTP y este método no lo hace.
Probablemente podrías simplemente sobrescribir la clase MockEmail, pero esto no me pareció un caso real. Quiero decir, quiero probar que la aplicación realmente puede enviar correos electrónicos a mis usuarios, por lo que necesito seguir el mismo camino, de lo contrario la prueba no tendría mucho sentido, ¿verdad?
Si tengo que probar que puedo recibir el email, no quiero un mock send(), ¿no?
Sobreescribiendo los setUpMethods de la clase de prueba
En la documentación, también se dice que se puede eliminar 'mockEmail' de la clase Test, dentro de $setUpMethods.
En el código de $setUpMethods hay este comentario de advertencia:
// CodeIgniter\Test\CIUnitTestCase.php /** * Métodos a ejecutar durante el setUp. * * ATENCIÓN: No anular a menos que sepas exactamente lo que estás haciendo. * Esta propiedad puede ser obsoleta en el futuro. * * @var array de métodos */ protected $setUpMethods = [ 'resetFactories', 'mockCache', 'mockEmail', 'mockSession', ];
Así que ir con esa solución parece desaconsejable. Pero, ¿qué podría ser un "comportamiento intrusivo" en la fase de pruebas? No estoy seguro. Tal vez si haces que las pruebas estén disponibles con llamadas públicas (get, post) podría haber un problema. Este no es mi caso.
Solución y prueba de trabajo
Mi solución es puentear la clase mocked, de la siguiente manera.
<?php // testsapp\Emails\SendEmailTest.php namespace App\Emails; use CodeIgniter\Test\CIUnitTestCase; final class SendEmailTest extends CIUnitTestCase { private $email = null; public function __construct() { parent::__construct(); // Eliminar el mockEmail de esta prueba $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('Prueba de correo electrónico'); $this->email->setMessage('Probando que el correo electrónico se puede enviar correctamente.'); $result = $this->email->send(); if (!$result) { d($this->email->printDebugger()); } $this->assertTrue($result); } }
Conclusiones
La pregunta es: ¿estoy en lo cierto al pensar que no es un comportamiento lógico y debería mejorarse, o hay una buena razón para hacerlo, que desconozco? ¿Hay alguna forma mejor de conseguirlo?
¡Déjame saber tus opiniones en los comentarios de abajo! Si te ha gustado este artículo, ¡sígueme en Facebook y Youtube!