Skip to content

Latest commit

 

History

History

modulo13-testes-automatizados

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Escrevendo testes automatizados

Escrendo nosso script de teste

Poderiamos fazer nossos testes criando um arquivo test_calculadora.py, onde iria fazer os testes da funções do arquivo calculadora.py:

from calculadora import somar

# Teste soma de dois numeros inteiros
if somar(2, 4) == 6:
    print("PASS")
else:
    print("FAIL")

# Teste soma de número com zero
if somar(2, 0) == 2:
    print("PASS")
else:
    print("FAIL")

E então executariamos nosso arquivo de testes:

python test_calculadora.py

Mas existem bibliotecas para testes em Python que veremos a seguir.

Utilizando a biblioteca unittest

Para usar a biblioteca unittest fazemos o seu import, criar uma nova classe que herda de unittest.TestCase, assim teremos os nossos casos de teste nessa classe e para executar os testes declarados usamos o unittest.main():

from calculadora import somar, dividir
import unittest

class TestSomar(unittest.TestCase):
    def test_soma_de_dois_numeros_inteiros(self):
        soma = somar(2, 4)
        self.assertEqual(soma, 6)

    def test_soma_de_numero_com_zero(self):
        self.assertEqual(somar(2, 0), 2)

class TestDividir(unittest.TestCase):
    def test_divide_numero_por_1_retorna_o_numero(self):
        self.assertEqual(dividir(10, 1), 10)

    def test_divide_por_zero_(self):
        self.assertEqual(dividir(10, 0), "Não é um número")

unittest.main()

Desenvolvimento orientado à testes (TDD)

Em suma é "escrever os testes antes da lógica". Para isso temos requisítos da aplicação a ser desenvolvida:

from datetime import timedelta

class Tarefa:
    def __init__(self, titulo, descricao="", data=None, data_notificacao=None):
        self.titulo = titulo
        self.descricao = descricao
        self.data = data
        self.data_notificacao = data_notificacao
        self.concluida = False

    def concluir(self):
        """
        Define essa tarefa como concluida.
        """
        pass

    def adicionar_descricao(self, descricao):
        """
        Adiciona uma descrição para a tarefa.
        """
        pass

    def adiar_notificacao(self, minutos):
        """
        Adia a notificação em uma certa quantidade de minutos.

        Notificacao: 25/02/2022, 14h30
        adiar_notificacao(15)
        => Notificacao: 25/02/2022, 14h45
        """
        pass

    def atrasada(self):
        """
        Diz se tarefa está atrasada. Ou seja, data < hoje.
        """
        pass

Vamos começar escrevendo o teste para uma tarefa concluída, que no caso esperamos que esperamos que seu atributo concluida seja True, então escrevemos o nosso testes, que na primeria execução dá erro, para só depois implementarmos a lógica, então teremos em um arquivo test_tarefa.py:

import unittest
from datetime import datetime
from tarefa import Tarefa

class TestConcluir(unittest.TestCase):
    def test_concluir_tarefa_altera_concluido_para_true(self):
        tarefa = Tarefa("Estudar Python")
        tarefa.concluir()
        self.assertEqual(tarefa.concluida, True)

unittest.main()

Ao executar teremos um erro, para solucionar esse problema, vamos implementar a função de concluir:

def concluir(self):
    """
    Define essa tarefa como concluida.
    """
    self.concluida = True

E dentro do case de testes de concluir, podemos ter outros testes:

class TestConcluir(unittest.TestCase):
    def test_concluir_tarefa_altera_concluido_para_true(self):
        tarefa = Tarefa("Estudar Python")
        tarefa.concluir()
        self.assertEqual(tarefa.concluida, True)

    def test_concluir_tarefa_concluida_mantem_concluida_como_true(self):
        tarefa = Tarefa("Estudar Python")
        tarefa.concluir()
        self.assertEqual(tarefa.concluida, True)
        # Concluir uma tarefa já concluida
        tarefa.concluir()
        self.assertEqual(tarefa.concluida, True)

Date e Datetime

No Python temos a biblioteca datetime para conseguirmos representar datas e datetime, para importamos:

from datetime import date, datetime

Alguns exemplos de uso são:

# retornar da date de hoje
date.today()

# retorna o dia
date.today().day

# retorna o mês
date.today().month

# retorna o ano
date.today().year

# também podemos usar um construtor para definir uma data
data = date(2022, 4, 2) # ano, mês e dia

# podemos fazer operações
data == date.today()
data > date.today()

# e temo o datetime para representar o tempo quando envolve horas, minutos, segundo e milisegundos
datetime.now() # retorna um objeto que tem ano, dia, mes, hora, minuto, segundo e milisegundos

# e também podemos usar o seu contrutor para definir um momento específico
agora = datetime.datetime(2022, 4, 2, 19) # perceba que posso ignorar alguns parâmetros, como hora, minuto, segundo e milisegundos. Ano, mês e dia são obrigatórios.

antes = datetime.datetime(2022, 4, 2)

# e também podemos fazer alguma operações
antes < agora

Testando com datetime

Vamos criar os testes para nossa aplicação nas funções que trabalham com datas:

def adiar_notificacao(self, minutos):
    """
    Adia a notificação em uma certa quantidade de minutos.

    Notificacao: 25/02/2022, 14h30
    adiar_notificacao(15)
    => Notificacao: 25/02/2022, 14h45
    """
    pass

Então vamos começar criando um novo case de testes e simples executar para ver :

def test_adia_notificacao_em_N_minutos(self):
    tarefa = Tarefa("Fazer passar esse teste")
    tarefa.adiar_notificacao(15)

Ao tentar executar os testes, teremos uma falha pois ainda não temos nada implementado. Então podemos fazer:

def adiar_notificacao(self, minutos):
    """
    Adia a notificação em uma certa quantidade de minutos.

    Notificacao: 25/02/2022, 14h30
    adiar_notificacao(15)
    => Notificacao: 25/02/2022, 14h45
    """
    if self.data_notificacao is None:
        return

    self.data_notificacao + minutos

Ao executar novamente os nossos testes, não teremos mais erros, mas ainda não fizemos o assert do nosso teste, para realmente fazer a validação, então:

class TestAdiarNotificacao(unittest.TestCase):
    def test_adia_notificacao_em_N_minutos(self):
        dt_original = datetime(2022, 2, 10, 9, 10)  # year, month, day, hour, minute, second, millisecond
        tarefa = Tarefa("Estudar Python", data_notificacao=dt_original)
        tarefa.adiar_notificacao(15)

        dt_esperado = datetime(2022, 2, 10, 9, 25)
        self.assertEqual(tarefa.data_notificacao, dt_esperado)

Dessa forma se tentarmos dessa forma como foi implementada a nossa lógia em adiar_notificacao, teremos outro erro, pois o atributo minuto é imutável ('datetime.datetime' objects is not writeble).

Para que possamos alterar o valor desse atributo usamos o timedelta pelo from datetime import timedelta, algumas operações possíveis são:

agora = datetime.now()

agora + timedelta(days=3) # para adicionar 3 dias
agora + timedelta(minutes=65) # para adicionar 65 minutos, assim a hora também é imcrementada em 1 hora e os munitos 5 

Assim o deltatime cria um novo objeto datetime, pois o objeto agora é imutável.

Para saber mais sobre o timedelta

Então podemos alterar nossa implementação para:

 def adiar_notificacao(self, minutos):
    """
    Adia a notificação em uma certa quantidade de minutos.

    Notificacao: 25/02/2022, 14h30
    adiar_notificacao(15)
    => Notificacao: 25/02/2022, 14h45
    """
    if self.data_notificacao is None:
        return

    self.data_notificacao = self.data_notificacao + timedelta(minutes=minutos)

Testando a Lista de Tarefas

Apenas um adendo para a implementação do teste das tarefas em atraso, pois utilizei o timedelta(seconds=1) para que a comparação das datas usando o datetime.now() estivesse 1 segundo a frente, caso o teste demore mais tempo para executar, aumente esse tempo.

class TestGetTarefasAtrasadas(unittest.TestCase):
    def test_retorna_lista_tarefas_atrasadas(self):
        hoje = datetime.now()
        tarefa_um = Tarefa("Tarefa Teste 1", data=hoje - timedelta(minutes=10))
        tarefa_dois = Tarefa("Tarefa Teste 2", data=hoje - timedelta(days=2))
        tarefa_tres = Tarefa("Tarefa Teste 3", data=hoje + timedelta(seconds=1))
        tarefa_quatro = Tarefa("Tarefa Teste 4", data=hoje + timedelta(days=1))
        lista = ListaDeTarefas()
        
        lista.adicionar_tarefa(tarefa_um)
        lista.adicionar_tarefa(tarefa_dois)
        lista.adicionar_tarefa(tarefa_tres)
        lista.adicionar_tarefa(tarefa_quatro)
        
        self.assertListEqual([tarefa_um, tarefa_dois], lista.get_tarefas_atrasadas())

Testando nosso projeto Django

Para iniciar nossos testes, vamos no diretório da aplicação agenda, lá teremos um arquivo tests.py que o próprio Django fornece. A partir dele vamos começar a escrever nossos testes.

Note que o import usado nesse caso é from django.test import TestCase, diferente do import unittest, que trás mais funcionalidades.

Para simular o nosso primeiro teste, vamos simular requisição HTTP do método GET de um cliente (geralmente um browser como Chrome ou Firefox), para isso precisamos importar de django.test a classe Client:

from django.test import TestCase, Client

Vamos criar o nosso teste, onde verifica se na página inicial tem um terminado elemento _ HTML_:

class TestPaginaInicial(TestCase):
    def test_lista_eventos(self):
        client = Client()
        response = client.get("/")
        print(response.content)
        self.assertContains(response,"""<h2 class="text-3xl font-bold mb-12 pb-4 text-center">Últimos Eventos</h2>""")

E agora para executar nosos testes o próprio Django vai fazer o Test Discovery, pois ele procurar por todos os arquivos tests.py e executa os testes. Para isso, usamos o comando:

python manage.py test

Usando o print(response.content), podemos ver o conteúdo da nossa resposta que inicia com um b que indica que é um código binário.

Esse teste é uma das abordagem possíveis. Outra forma é verificar se página inicial está usando o template correto. Pois se mantermos a da forma como está, todas vez que fizermos uma alteração no nosso template precisaremos alterar também o nosso teste para que ele não quebre.

class TestPaginaInicial(TestCase):
    def test_lista_eventos(self):
        client = Client()
        response = client.get("/")
        self.assertTemplateUsed(response, "agenda/listar_eventos.html")

Testando a listagem de eventos

Vamos fazer os testes baseados no Happy Path, caminho "caminho feliz" ou o "caminho esperado".

Uma informação importante sobre a execução de testes no Django é que ele cria, por padrão, um banco de dos específico para a execução de cada teste e ao final esse banco é destruido, assim podemos fazer testes que envolvam persistência de dados sem influenciar outros testes ou em nossa base de dados real, seja ela de produção ou desenvolvimento.

Django

Assim podemos fazer o nosso teste da sequinte forma:

class TestListagemDeEventos(TestCase):
    def test_evento_com_data_de_hoje_e_exibido(self):
        categoria = Categoria()
        categoria.nome = "Back-end"
        categoria.save()
        
        evento = Evento()
        evento.nome = "Aula de Python"
        evento.categoria = categoria
        evento.local = "Sinop"
        evento.data = date.today()
        evento.save()
        
        client = Client()
        response = client.get("/")
        self.assertContains(response, "Aula de Python")

Onde verificamos de no conteúdo da resposta tem a String Aula de Python.

Outra forma é usando o context do reponse:

class TestListagemDeEventos(TestCase):
    def test_evento_com_data_de_hoje_e_exibido(self):
        categoria = Categoria()
        categoria.nome = "Back-end"
        categoria.save()
        
        evento = Evento()
        evento.nome = "Aula de Python"
        evento.categoria = categoria
        evento.local = "Sinop"
        evento.data = date.today()
        evento.save()
        
        client = Client()
        response = client.get("/")
        self.assertEqual(response.context["eventos"][0], evento)

No context do response temos um QuerySet e podemos acessar o seu índice e verificar se é igual ao evento criado.

Outra forma possível é:

class TestListagemDeEventos(TestCase):
    def test_evento_com_data_de_hoje_e_exibido(self):
        categoria = Categoria()
        categoria.nome = "Back-end"
        categoria.save()
        
        evento = Evento()
        evento.nome = "Aula de Python"
        evento.categoria = categoria
        evento.local = "Sinop"
        evento.data = date.today()
        evento.save()
        
        client = Client()
        response = client.get("/")
        self.assertEqual(response.context["eventos"], [evento])

Mas se tentarmos sem converter o QuerySet em uma list, teremos o erro:

Django

Para corrigir esse erro, faça:

class TestListagemDeEventos(TestCase):
    def test_evento_com_data_de_hoje_e_exibido(self):
        categoria = Categoria()
        categoria.nome = "Back-end"
        categoria.save()
        
        evento = Evento()
        evento.nome = "Aula de Python"
        evento.categoria = categoria
        evento.local = "Sinop"
        evento.data = date.today()
        evento.save()
        
        client = Client()
        response = client.get("/")
        self.assertEqual(list(response.context["eventos"]), [evento])

Expondo um bug com testes

Vamos criar um teste para um caso de borda ou edge case, vamos incluir mais um teste em nossa suité de testes, class TestListagemDeEventos(TestCase):

def test_eventos_sem_data_sao_exibidos(self):
    categoria = Categoria()
    categoria.nome = "Back-end"
    categoria.save()
    
    evento = Evento()
    evento.nome = "Aula de Python"
    evento.categoria = categoria
    evento.local = "Sinop"
    evento.data = None
    evento.save()
    
    client = Client()
    response = client.get("/")
    self.assertContains(response, "Aula de Python")
    self.assertContains(response, "A definir")
    self.assertEqual(list(response.context["eventos"]), [evento])

Antes de executar nosso teste, vamos alterar nossa regra para listar os eventos em nossa agenda/views.py de:

def listar_eventos(request):
    eventos = Evento.objects.filter(data__gte=date.today()).order_by('data')
    
    for evento in eventos:
        evento.random = '{:0>3}'.format(randrange(1, 120))
    
    get_random = '{:0>3}'.format(randrange(120))
    return render(
        request=request, 
        context={ "eventos": eventos, 'get_random': get_random }, 
        template_name="agenda/listar_eventos.html"
    )

Para:

def listar_eventos(request):
    eventos = Evento.objects.exclude(data__lt=date.today()).order_by('data')
    
    for evento in eventos:
        evento.random = '{:0>3}'.format(randrange(1, 120))
    
    get_random = '{:0>3}'.format(randrange(120))
    return render(
        request=request, 
        context={ "eventos": eventos, 'get_random': get_random }, 
        template_name="agenda/listar_eventos.html"
    )

Ao executar nossos testes, temos um erro onde não foi encontrado "A definir", pois não cumprimos corretamente um dos nossos requisitos:

Django