Ícone do site Mailtrap

Como Testar Emails em Seu App Python

Python test email

O Python oferece várias maneiras de testar emails. Ele possui opções nativas e a capacidade de se integrar com uma ferramenta de terceiros, como o Mailtrap Email Testing.

Recentemente, explorei algumas abordagens e vou compartilhar minha experiência neste tutorial. Cobrirei métodos nativos como testes unitários + aiosmtpd, a função subprocess do Python, bem como a biblioteca de objetos mock dos testes unitários.

Também demonstrarei como integrar e testar emails em Python com o Mailtrap Email Testing.

Formas nativas de testar emails em Python

Vamos começar com as formas nativas de testar emails em Python.

Usando testes unitários e aiosmtpd

Aiosmtpd é uma biblioteca que permite configurar um servidor SMTP (Simple Mail Transfer Protocol) local. Isso criará um ambiente de teste e gerenciará o tráfego de emails internamente. Assim, seus emails de teste não chegarão aos destinatários. Consulte a página do GitHub do aiosmtpd para mais informações.

Vou criar um teste para um email de texto simples.

Pré-requisitos:

Nota: Adicionei os trechos de código abaixo no arquivo tests.py.

Para instalar o aiosmtpd, execute o comando pip install aiosmtpd.

Execute o servidor com o comando python -m aiosmtpd -n.

Usarei o framework de teste unitário, uma biblioteca padrão em todas as versões do Python desde a 2.1, e a biblioteca smtplib. O módulo smtplib usa o protocolo RFC 821 padrão para formatar mensagens.

from unittest import TestCase
import smtplib

Crie uma classe que herdará o teste unitário do objeto TestCase e configure o caso de teste.

class EmailTestCase(TestCase):
    def test_send_email(self):
        sender_email = "seu_email@gmail.com"
        receiver_email = "destinatario_email@exemplo.com"
        message = "Este é um email de teste."

As variáveis sender_email e receiver_email podem ser preenchidas com dados de exemplo, pois estamos simulando o envio de email. A variável message deve conter o corpo do email desejado.

O próximo passo é enviar um email de teste usando o smtplib.

with smtplib.SMTP(host="localhost", port=8025) as server:
            server.ehlo()
            server.sendmail(sender_email, receiver_email, message)

Aqui, criamos uma nova instância de um cliente SMTP. Por padrão, o servidor funciona em localhost com o número da porta 8025. Também iniciamos um handshake SMTP com EHLO e adicionamos o método sendmail para enviar os emails. O método sendmail recebe três argumentos – sender_email, receiver_email e message.

O script agora está pronto. Aqui está o exemplo completo:

from unittest import TestCase
import smtplib

class EmailTestCase(TestCase):
    def test_send_email(self):
        sender_email = "seu_email@gmail.com"
        receiver_email = "destinatario_email@exemplo.com"
        message = "Este é um email de teste."
       
        with smtplib.SMTP(host="localhost", port=8025) as server:
            server.ehlo()
            server.sendmail(sender_email, receiver_email, message)

Para executar o código Python, use o botão Run.

Se o teste passar, você verá uma mensagem semelhante à abaixo.

Você também verá que o email foi enviado no terminal.

Limitações dessa abordagem

Embora essa abordagem possa ser útil para testar scripts simples de envio de email, descobri que ela possui várias limitações:

Usando a função subprocess do Python com testes unitários e aiosmtpd

Uma maneira de automatizar processos ao usar testes unitários e aiosmtpd é a função subprocess do Python. Ela nos permite executar um programa externo a partir do script Python.

Primeiro, tive que importar a função subprocess e o módulo time para melhorar e aprimorar o script anterior. Usarei o último para aguardar alguns segundos antes que o servidor esteja pronto.

from unittest import TestCase
import smtplib
import subprocess
import time 

Depois, adicionei o método setUp. Ele prepara o ambiente para executar o servidor. A função subprocess.Popen executará o comando em um novo processo.

Nesse caso, o comando é exec python -m aiosmtpd -n, significando que o servidor será executado em um novo processo.

shell=True permitirá que executemos o comando usando o shell. Não precisaremos criar um novo terminal. Em vez disso, o processo será executado em segundo plano.

Como mencionado, time.sleep(2) pausará a execução do script por 2 segundos para dar tempo suficiente para o servidor ficar pronto.

def setUp(self):
    self.process = subprocess.Popen(args='exec python -m aiosmtpd -n', shell=True)
    time.sleep(2)

O próximo passo é adicionar um método tearDown, que encerrará o subprocesso e aguardará o término do processo.

def tearDown(self):
    self.process.kill()
    self.process.wait()

O teste em si é essencialmente o mesmo, mas adicionei uma asserção a mais. Ela verifica se o servidor está funcionando e se o socket está aberto.

self.assertIsNotNone(server.sock)

Aqui está o exemplo completo de código:

from unittest import TestCase
import smtplib
import subprocess
import time

class EmailTestCase(TestCase):
    def setUp(self):
        self.process = subprocess.Popen(args='exec python -m aiosmtpd -n', shell=True)
        time.sleep(2)

    def test_send_email(self):
        sender_email = "seu_email@gmail.com"
        receiver_email = "destinatario_email@exemplo.com"
        message = "Este é um email de teste."
        with smtplib.SMTP(host="localhost", port=8025) as server:
            server.ehlo()
            server.sendmail(sender_email, receiver_email, message)
            self.assertIsNotNone(server.sock)

    def tearDown(self):
        self.process.kill()
        self.process.wait()

Neste ponto, podemos executar o script com o botão Run.

Os testes foram bem-sucedidos, significando que o servidor foi ligado e desligado conforme esperado.

Executando o script a partir do console

Como você notará, usei o botão Run para executar os scripts nos exemplos anteriores. Vou adicionar a instrução if para executar o código a partir do console, modificar a importação do teste unitário e referenciar o unittest.TestCase com a definição da classe EmailTestCase.

import unittest
import smtplib
import subprocess
import time

class EmailTestCase(unittest.TestCase):

    def setUp(self):
        self.process = subprocess.Popen(args='exec python -m aiosmtpd -n', shell=True)
        time.sleep(2)

    def test_send_email(self):
        sender_email = "seu_email@gmail.com"
        receiver_email = "destinatario_email@exemplo.com"
        message = "Este é um email de teste."

        with smtplib.SMTP(host="localhost", port=8025) as server:
            server.ehlo()
            server.sendmail(sender_email, receiver_email, message)
            self.assertIsNotNone(server.sock)

    def tearDown(self):
        self.process.kill()
        self.process.wait()

if __name__ == '__main__':
    unittest.main()

Com a configuração atualizada, agora podemos executar o código diretamente do console usando o comando python tests.py.

Testando se o script pode ler arquivos

A última modificação no script foi habilitar a leitura de emails a partir de um arquivo. Isso me permitiria testar se as variáveis no template foram substituídas corretamente. Optei por uma configuração simples novamente.

Então, criei um novo arquivo de template com apenas uma variável de mensagem.

Voltei ao arquivo tests.py e adicionei uma instrução with que seria responsável por abrir e fechar arquivos.

with open('template.html') as file:
    template = file.read()

Usando a função format, adicionei a mensagem ao template.

template = template.format(message=message)

E inseri o template na função sendmail.

server.sendmail(sender_email, receiver_email, template)

O script completo ficará assim:

import unittest
import smtplib
import subprocess
import time

class EmailTestCase(unittest.TestCase):

    def setUp(self):
        self.process = subprocess.Popen(args='exec python -m aiosmtpd -n', shell=True)
        time.sleep(2)

    def test_send_email(self):
        sender_email = "seu_email@gmail.com"
        receiver_email = "destinatario_email@exemplo.com"
        message = "Este é um email de teste."

        with open('template.html') as file:
            template = file.read()

        template = template.format(message=message)
        with smtplib.SMTP(host="localhost", port=8025) as server:
            server.ehlo()
            server.sendmail(sender_email, receiver_email, template)
            self.assertIsNotNone(server.sock)

    def tearDown(self):
        self.process.kill()
        self.process.wait()

if __name__ == '__main__':
    unittest.main()

Execute o código com python test.py e verifique o resultado. A variável de mensagem foi substituída corretamente, então o teste foi bem-sucedido.

Limitações dessa abordagem

Semelhante ao uso de aiosmtpd e testes unitários, essa abordagem expandida também possui suas limitações:

Usando a biblioteca de objetos mock dos testes unitários

Outra opção para testar emails nativamente em Python é a biblioteca de objetos mock dos testes unitários. Ela permite que você faça uma espécie de imitação (mock) da conexão do servidor SMTP sem enviar os emails.

Aqui está o script:

import unittest
from email.mime.text import MIMEText
from unittest.mock import patch
import smtplib

def send_email(server, port, subject, message, from_addr, to_addr):
   
    smtp_user = 'username'
    smtp_password = 'senha'
    msg = MIMEText(message)
    msg['From'] = from_addr
    msg['To'] = to_addr
    msg['Subject'] = subject
    with smtplib.SMTP(server, port) as server:
        server.starttls()
        server.login(smtp_user, smtp_password)
        server.send_message(msg)
class TestEmailSending(unittest.TestCase):
    @patch('smtplib.SMTP')
    def test_send_email(self, mock_smtp):
        # Arrange: Defina nossas expectativas
        subject = "Assunto de Teste"
        message = "Olá, este é um teste."
        from_addr = 'de@exemplo.com'
        to_addr = 'para@exemplo.com'
        server = "sandbox.smtp.mailtrap.io"
        port = 587
        # Act: Chame a função send_email
        send_email(server, port, subject, message, from_addr, to_addr)
       
        # Assert: Verifique se as chamadas corretas foram feitas no objeto SMTP
        mock_smtp.assert_called_with(server, port)
        instance = mock_smtp.return_value.__enter__.return_value
        instance.send_message.assert_called_once()
        call_args = instance.send_message.call_args[0]
        sent_email = call_args[0]
       
        # Verifique o conteúdo do email
        self.assertEqual(sent_email['Subject'], subject)
        self.assertEqual(sent_email['From'], from_addr)
        self.assertEqual(sent_email['To'], to_addr)
        self.assertEqual(sent_email.get_payload(), message)

if __name__ == '__main__':
    unittest.main()

O código fornecido define uma função e inclui uma classe de teste, TestEmailSending, usando o framework de teste unitário do Python para testar essa função.

A função send_email recebe detalhes do servidor, assunto, corpo da mensagem, endereço do remetente e endereço do destinatário como parâmetros, cria um objeto de email (MIMEText) com esses detalhes e, em seguida, faz login em um servidor SMTP para enviá-lo.

No caso de teste, a classe smtplib.SMTP é mockada usando unittest.mock.patch, permitindo que o teste verifique se o servidor SMTP é chamado com os parâmetros corretos sem realmente enviar um email.

O teste verifica se o método send_message da instância SMTP é chamado corretamente e afirma que o assunto, o endereço do remetente, o endereço do destinatário e o payload do email correspondem aos valores esperados.

Limitações dessa abordagem

Como testar emails em Python com o Mailtrap via SMTP

Para abordar as limitações dos métodos nativos, decidi usar o Mailtrap Email Testing. É um Sandbox de Email que fornece um ambiente seguro para inspecionar e depurar emails em ambientes de desenvolvimento, QA e staging.

Usei o Email Testing para executar testes funcionais e de entregabilidade, verificar a formatação do email, analisar o conteúdo para spam, verificar anexos e testar o tratamento de erros.

Você precisará de uma conta para usá-lo, e você pode criá-la rapidamente abrindo a página de inscrição. Depois que a conta estiver ativa, podemos ir direto ao teste.

Testes funcionais

Precisaremos de um script ligeiramente diferente neste caso. Crie uma nova classe EmailTestCase e adicione dados de exemplo. Aqui, test_mailtrap é um método que enviará um email de teste para o Mailtrap.

import unittest

class EmailTestCase(unittest.TestCase):

    def test_mailtrap(self):
        sender_email = 'remetente@exemplo.com'
        recipient_email = 'destinatario@exemplo.com'
        subject = 'Email de Teste'
        body = 'Este é um email de teste enviado pelo servidor SMTP.'

O próximo passo é importar o objeto EmailMessage incorporado.

from email.message import EmailMessage 

Usando-o, vamos construir uma mensagem e adicionar os dados de exemplo a ela.

msg = EmailMessage()
msg['From'] = sender_email
msg['To'] = recipient_email
msg['Subject'] = subject
msg.set_content(body)

O último passo é conectar-se ao servidor SMTP falso do Mailtrap Email Testing. Para isso, usarei smtplib e a instrução with. Neste ponto, precisarei das credenciais SMTP do Email Testing. Para acessá-las, vá para Email Testing → My Inbox → SMTP Settings e clique em Show Credentials.

Copie o Host, Username e Password, e retorne ao projeto Python. Crie uma nova instância do cliente SMTP e conecte-se ao servidor usando o host e a porta.

Chame o método server.login e passe um nome de usuário e senha para ele.

with smtplib.SMTP(host="sandbox.smtp.mailtrap.io", port=2525) as server:
    server.login(user="seu_username", PASSWORD)
    server.send_message(msg)

Nota: seu_username é um placeholder. Você deve adicionar seu nome de usuário real aqui.

Por motivos de segurança, armazenei a senha em uma pasta separada. Então, vou importá-la.

from variables import PASSWORD 

O script de teste de email agora está pronto.

import unittest
import smtplib

from email.message import EmailMessage

from variables import PASSWORD

class EmailTestCase(unittest.TestCase):

    def test_mailtrap(self):
        sender_email = 'remetente@exemplo.com'
        recipient_email = 'destinatario@exemplo.com'
        subject = 'Email de Teste'
        body = 'Este é um email de teste enviado pelo servidor SMTP.'
        msg = EmailMessage()
        msg['From'] = sender_email
        msg['To'] = recipient_email
        msg['Subject'] = subject
        msg.set_content(body)

        with smtplib.SMTP(host="sandbox.smtp.mailtrap.io", port=2525) as server:
            server.login(user="seu_username", PASSWORD)
            server.send_message(msg)

if __name__ == '__main__':
    unittest.main()

Executei o código com o comando python tests.py. E o email chegou à caixa de entrada do Email Testing em segundos.

Resultados do teste:

Verificando a formatação do email e o código HTML

Os próximos passos nos meus testes foram:

Para isso, enviei um email HTML simples para minha caixa de entrada virtual usando as seguintes linhas de código. Usei um script de envio de email, não um teste unitário neste caso, já que o email ainda seria capturado na caixa de entrada. Para mais informações sobre como enviar emails HTML a partir do Python, consulte este artigo no blog.

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

port = 2525
smtp_server = "sandbox.smtp.mailtrap.io"
login = "seu_username"
password = "sua_senha"

sender_email = "remetente@exemplo.com"
receiver_email = "destinatario@exemplo.com"
message = MIMEMultipart("alternative")
message["Subject"] = "Teste HTML"
message["From"] = sender_email
message["To"] = receiver_email

# escrever a parte text/plain
text = """\
Olá,
Confira o novo post no blog do Mailtrap:
Servidor SMTP para Testes: Baseado em Nuvem ou Local?
https://blog.mailtrap.io/2018/09/27/cloud-or-local-smtp-server/
Sinta-se à vontade para nos informar que conteúdo seria útil para você!"""

# escrever a parte HTML
html = """\
<html>
  <body>
    <p>Olá,<br>
      Confira o novo post no blog do Mailtrap:</p>
    <p><a href="https://blog.mailtrap.io/2018/09/27/cloud-or-local-smtp-server">Servidor SMTP para Testes: Baseado em Nuvem ou Local?</a></p>
    <p>Sinta-se à vontade para <strong>nos informar</strong> que conteúdo seria útil para você!</p>
  </body>
</html>
"""

# converter ambas as partes em objetos MIMEText e adicioná-las à mensagem MIMEMultipart
part1 = MIMEText(text, "plain")
part2 = MIMEText(html, "html")
message.attach(part1)
message.attach(part2)

with smtplib.SMTP("sandbox.smtp.mailtrap.io", 2525) as server:
    server.login(login, password)
    server.sendmail(
        sender_email, receiver_email, message.as_string()
    )

print('Enviado')

Resultados do teste:

Teste de entregabilidade

Para estender meus testes, abri a aba Spam Analysis. Isso mostrou o Spam Report com a pontuação de spam, bem como os pontos de spam com descrições. Isso é útil para melhorar o template se ele ultrapassar o limite de 5. Os testes de spam são executados com a ajuda do filtro SpamAssassin.

Resultados do teste:

Verificando os anexos

Também queria ver se o script de envio de email anexaria, codificaria e enviaria anexos corretamente. Anexos são objetos MIME. A codificação implica o uso de um módulo base64 que codifica todos os dados binários com caracteres ASCII.

Simplesmente adicionei as credenciais SMTP do Email Testing a este script e esperei que o email chegasse na caixa de entrada virtual.

import smtplib

from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

port = 2525
smtp_server = "sandbox.smtp.mailtrap.io"
login = "seu_username"
password = "sua_senha"

subject = "Um exemplo de cartão de embarque"
sender_email = "mailtrap@exemplo.com"
receiver_email = "novo@exemplo.com"

message = MIMEMultipart()
message["From"] = sender_email
message["To"] = receiver_email
message["Subject"] = subject

body = "Este é um exemplo de como você pode enviar um cartão de embarque em anexo com Python"
message.attach(MIMEText(body, "plain"))

filename = "seuCE.pdf"

with open(filename, "rb") as attachment:
    part = MIMEBase("application", "octet-stream")
    part.set_payload(attachment.read())

encoders.encode_base64(part)

part.add_header(
    "Content-Disposition",
    f"attachment; filename= {filename}",
)

message.attach(part)
text = message.as_string()

with smtplib.SMTP("sandbox.smtp.mailtrap.io", 2525) as server:
    server.login(login, password)
    server.sendmail(
        sender_email, receiver_email, text
    )
print('Enviado')

Este trecho de código envia um email com um anexo PDF. Supõe-se que o arquivo esteja localizado no mesmo diretório em que você executa seu script Python. O método attach adiciona o anexo à mensagem e o converte em uma string.

Com um método semelhante, você também pode testar outros tipos de anexos. Nesse caso, você deve usar uma classe apropriada, como email.mime.audio.MIMEAudio ou email.mime.image.MIMEImage. Leia a documentação do Python para mais informações.

Veja como ficará na caixa de entrada do Email Testing.

Resultados do teste:

Testando templates

Em seguida, testei meus templates de email com a API do Mailtrap, que me permite alternar do staging para a produção quando estiver pronto para começar a enviar.

Tudo o que fiz foi habilitar o sandbox, especificar o ID da caixa de entrada para receber o teste do template e depois enviá-lo através da API no ambiente de produção.

Testando o tratamento de erros

O último teste que executei com SMTP foi verificar o tratamento de erros. Enviei emails usando o módulo smtplib novamente, mas desta vez com blocos try e except.

import smtplib
from socket import gaierror

port = 2525
smtp_server = "sandbox.smtp.mailtrap.io"
login = "seu_username"
password = "sua_senha"
sender = "remetente@exemplo.com"
receiver = "destinatario@exemplo.com"
message = f"""\
Assunto: Email de Teste
To: {receiver}
From: {sender}

Esta é minha mensagem de teste com Python."""

try:
    with smtplib.SMTP(smtp_server, port) as server:
        server.login(login, password)
        server.sendmail(sender, receiver, message)
    print('Enviado')
except (gaierror, ConnectionRefusedError):
    print('Falha ao conectar ao servidor. Configurações de conexão ruins?')
except smtplib.SMTPServerDisconnected:
    print('Falha ao conectar ao servidor. Usuário/senha incorretos?')
except smtplib.SMTPException as e:
    print('Ocorreu um erro SMTP: ' + str(e))

Este script irá:

Como testar emails em Python com o Mailtrap via API

O Mailtrap Email Testing também oferece a opção de testar seus emails usando API. É baseado em princípios REST e retorna chamadas como objetos JSON. Todos os detalhes sobre a API do Email Testing são abordados na documentação da API.

Para fazer solicitações de API, você precisará do seu token de API e ID da caixa de entrada.

  1. Vá para Settings → API Tokens e clique em Add Token.
  1. Crie um token para o projeto e a caixa de entrada desejados.
  1. Uma vez que o token esteja pronto, vá para a caixa de entrada desejada. Verifique a URL – o ID da caixa de entrada é o número de 7 dígitos entre inboxes/ e /messages.

Uma solicitação de API de exemplo se parece com isso:

import http.client

conn = http.client.HTTPSConnection("sandbox.api.mailtrap.io")

payload = "{\n  \"to\": [\n    {\n      \"email\": \"joao_doe@exemplo.com\",\n      \"name\": \"João Doe\"\n    }\n  ],\n  \"cc\": [\n    {\n      \"email\": \"jane_doe@exemplo.com\",\n      \"name\": \"Jane Doe\"\n    }\n  ],\n  \"bcc\": [\n    {\n      \"email\": \"james_doe@exemplo.com\",\n      \"name\": \"Jim Doe\"\n    }\n  ],\n  \"from\": {\n    \"email\": \"vendas@exemplo.com\",\n    \"name\": \"Equipe de Vendas Exemplo\"\n  },\n  \"attachments\": [\n    {\n      \"content\": \"PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KCiAgICA8aGVhZD4KICAgICAgICA8bWV0YSBjaGFyc2V0PSJVVEYtOCI+CiAgICAgICAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJJRT1lZGdlIj4KICAgICAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEuMCI+CiAgICAgICAgPHRpdGxlPkRvY3VtZW50PC90aXRsZT4KICAgIDwvaGVhZD4KCiAgICA8Ym9keT4KCiAgICA8L2JvZHk+Cgo8L2h0bWw+Cg==\",\n      \"filename\": \"index.html\",\n      \"type\": \"text/html\",\n      \"disposition\": \"attachment\"\n    }\n  ],\n  \"custom_variables\": {\n    \"user_id\": \"45982\",\n    \"batch_id\": \"PSJ-12\"\n  },\n  \"headers\": {\n    \"X-Message-Source\": \"dev.mydomain.com\"\n  },\n  \"subject\": \"Sua Confirmação de Pedido Exemplo\",\n  \"text\": \"Parabéns pelo seu pedido nº 1234\",\n  \"category\": \"API Test\"\n}"

headers = {
    'Content-Type': "application/json",
    'Accept': "application/json",
    'Api-Token': "seu_token_api"
}

conn.request("POST", "/api/send/seu_id_de_caixa_de_entrada", payload, headers)

res = conn.getresponse()
data = res.read()

print(data.decode("utf-8"))

Mais uma vez, examine a documentação da API e esta página, em particular, para aprender como enviar solicitações de API em Python (ou outras linguagens), enviar uma solicitação de exemplo e verificar a resposta de exemplo.

Concluindo

Os métodos nativos do Python são suficientes para testar o envio de emails e executar outros testes funcionais. No entanto, para verificar a pontuação de spam, a conexão com servidores externos ou HTML/CSS, você precisará de uma ferramenta de teste dedicada, como o Mailtrap Email Testing.

Se você quiser aprender mais sobre envio e teste de emails em Python, confira nossos artigos no blog:

Também abordamos o envio de emails em frameworks populares do Python, como Django e Flask.

Fique de olho em nosso blog, pois publicamos mais tópicos sobre testes: como testar emails, testes de email para iniciantes, sandboxing de email, entre outros.

Não deixe que a cobra do Python destrua seus emails. Boa sorte!

Sair da versão mobile