Testes unitários são basicamente umas das formas de saber se o código da aplicação que está sendo construída está no caminho correto, baseado nos requerimentos.
Normalmente, e de forma correta, se constrói os testes para depois criar o código da aplicação, usando o teste em si como referência de funcionalidade.
Como os testes funcionam
Demorou para entrar na minha cabeça como os testes unitários funcionam. Eu sempre me perguntava: — Como assim, primeiro você cria os testes para depois criar o código? Isso não entra na minha cabeça!
Isso é bem verdade, porém vamos usar uma analogia usando como exemplo uma fábrica metalúrgica com uma linha de montagem:
Por exemplo uma empresa recebe o requisito para criar uma peça com todas as dimensões como requisitos, altura, largura e também o diâmetro da parede da mesma.
Essas requisições são super importantes para que a peça se encaixe corretamente em um motor ou outra engrenagem, sendo assim, ficando perfeitamente adequada para os requisitos do cliente.
Sendo torneiro mecânico em 2008 precisei usar testes para praticamente todas as peças que eu produzia para que as mesmas ficassem dentro do padrão do desenho/gabarito enviado pelo cliente.
Era usado um molde com as especificações corretas onde se encaixavam as peças que eram feitas. Assim, junto com as medidas especificadas se sabia se o produto feito estava correto.
Dessa forma ficou mais fácil entender o porquê de criar os testes primeiro para depois o código. Os testes são como o gabarito, o código são as peças criadas que serão testadas baseadas nos testes criados anteriormente (gabarito). Agora, creio que ficou mais fácil entender.
Testando uma aplicação REST API Django
Agora, vamos supor que recebemos um requisito para dois endpoints de uma aplicação de turismo:
- Comments
- Destinations
Os requisitos são como a seguir:
Como podemos ver, o modelo Comment tem um relacionamento de um para muitos com a tabela Destinations assim como os seus respectivos campos. Sendo assim, iniciaremos os testes para os modelos representados acima:
tourism/tests/test_models.py
# tourism/tests/test_models.py
from django.test import TestCase
from .models import Destinations, Comment
from django.utils import timezone
class TestModels(TestCase):
def test_destinations_model(self):
#test if Destinations model is passing correctly its values
destination = Destinations.objects.create(
tour_title = 'test1',
booking_start_date='2021-05-30',
booking_end_date= '2022-05-30',
price=2000,
description='test1',
author='test1',
)
destination.save()
self.assertEquals(destination.tour_title, 'test1')
self.assertEquals(destination.booking_start_date, '2021-05-30')
self.assertEquals(destination.booking_end_date, '2022-05-30')
self.assertEquals(destination.price, 2000)
self.assertEquals(destination.description, 'test1')
self.assertEquals(destination.author, 'test1')
def test_comment_model(self):
#test if Comment model is related to Destinations model
destination = Destinations.objects.create(
tour_title = 'test1',
booking_start_date ='2021-05-30',
booking_end_date= '2022-05-30',
price = 2000,
description = 'test1',
author = 'test1',
)
destination.save()
comment = Comment.objects.create(
#the relation between both models
post = destination,
name = 'John',
email = 'any@email.com',
comment = 'test1',
created_on = timezone.now(),
active = False,
)
comment.save()
self.assertEquals(comment.post, destination)
PythonComo visto acima, foi herdada a classe TestCase do Django para poder testar os dois modelos Comment e Destinations na classe TestModels.
Na função test_destinations_model(self) foi alocado o modelo Destinations em seu escopo, com uma query de criação feita no banco de dados de testes, usando o ORM do Django e com seus respectivos campos.
Logo abaixo, é usado a função asserEquals(param1, param2) para saber se a mesma está sendo criada corretamente. Neste caso a função assertEquals() serve como o gabarito exemplificado na analogia e exemplo mencionado acima, para testar se o mesmo está sendo criado como deveria no banco de dados de testes.
Vamos testar para ver se o teste retorna como correto, “OK”:
python manage.py test
Found 1 test(s).
System check identified no issues (0 silenced).
E
====================================================================
ERROR: tourism.tests (unittest.loader._FailedTest)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ImportError: Failed to import test module: tourism.tests
Traceback (most recent call last):
…
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Ran 1 test in 0.000s
BashComo visto, ao tentar rodar os testes o mesmo não funcionou pois não tem oque testar ainda, as classes/tabelas ainda não foram criadas.
Agora vamos criar as tabelas como no requerimento e seus consecutivos relacionamentos e rodar os testes novamente:
tourism/models.py:
# tourism/models.py
from django.db import models
# Create your models here.
class Destinations(models.Model):
author = models.CharField(max_length=200, unique=False)
tour_title = models.CharField(max_length=250)
description = models.TextField()
location = models.CharField(max_length=250)
booking_start_date = models.DateField()
booking_end_date = models.DateField()
price = models.DecimalField(max_digits=6, decimal_places=2)
def __str__(self):
return str(self.tour_title)
class Comment(models.Model):
post = models.ForeignKey(Destinations,
on_delete=models.CASCADE,related_name='comments')
name = models.CharField(max_length=80)
email = models.EmailField()
comment = models.TextField()
created_on = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=False)
def __str__(self):
return str(self.name)
PythonDepois de criar os modelos e fazer as migrations vamos testar para ver se os valores batem com os testes:
python manage.py test
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
--------------------------------------------------------------------
Ran 2 tests in 0.005s
OK
Destroying test database for alias 'default'...
BashCriando testes para os endpoints da aplicação REST API
Agora para testar os endpoints desta aplicação sera necessário criar a representação dos dados da mesma forma que foi efetuado para testar os modelos.
E também a representação dos dados e URLs para testar o CRUD em cada endpoint (Destinations e Comments).
Criando esse setup dentro da classe de testes, sera necessário criar funções que testam cada operação CRUD dos endpoints.
No código abaixo foi criado quatro funções para cada endpoint usando os dados adicionados ao método Setup(), sendo assim, testando cada método de requisição Create, Read, Update e Delete:
tourism/tests/test_api.py:
# tourism/tests/test_api.py
import json
from rest_framework import status
from django.utils import timezone
from django.http import response
from django.urls import reverse
from tourism.models import Destinations, Comment
from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient, APITestCase
class TourismTestCase(APITestCase):
def setUp(self):
self.headerInfo = {'content-type': 'application/json'}
# PASSO 1
# Criação de objetos que serão
# utilizados nos testes dos endpoints.
# Instance do objeto Destinations.
self.destination = Destinations.objects.create(
tour_title = 'test1',
booking_start_date='2021-05-30',
booking_end_date= '2022-05-30',
price=2000,
description='test1',
location='somewhere',
author='test1',
)
self.destination.save()
# Instance do objeto Comment.
self.comment = Comment.objects.create(
#the relation between both models
post = self.destination,
name = 'John',
email = 'any@email.com',
comment = 'test1',
created_on = timezone.now(),
active = False,
)
self.comment.save()
# PASSO 2
# Criação de dados para serem usados nos testes.
self.destination_data = {
'tour_title' :'test1',
'booking_start_date':'2021-05-30',
'booking_end_date':'2022-05-30',
'price':2000,
'description':'test1',
'location':'somewhere',
'author':'test1'
}
self.comment_data = {
'post':self.destination.id,
'name':'John',
'email':'any@email.com',
'comment':'test1',
'created_on':timezone.now(),
'active':False,
}
# PASSO 3
# Criação da representação dos endpoints
# que serão utilizados nos testes.
self.url_destination_list = reverse('tourism:destinations-list')
self.url_destination_detail = reverse('tourism:destinations-detail',
kwargs={'pk': self.destination.pk})
self.url_comment_url_list = reverse('tourism:comments-list')
self.url_comment_detail = reverse('tourism:comments-detail',
kwargs={'pk': self.comment.pk})
# PASSO 4
# Usar os dados criados no setUp()
# para criar as funções de teste.
def test_get_destination(self):
"""GET method"""
response = self.client.get(self.url_destination_list, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_create_destination(self):
""" Test POST method for Destination endpoint"""
response = self.client.post(
self.url_destination_list, self.destination_data,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_update_destination(self):
""" Test PUT method for Destination endpoint"""
response = self.client.put(
self.url_destination_detail, self.destination_data,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_delete_destination(self):
""" Test DELETE method for Destination endpoint"""
response = self.client.delete(
self.url_destination_detail, format='json'
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
def test_get_comment(self):
"""GET method"""
response = self.client.get(self.url_comment_url_list, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_create_comment(self):
""" Test POST method for Comment endpoint"""
response = self.client.post(
self.url_comment_url_list, self.comment_data,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_update_comment(self):
""" Test PUT method for Comment endpoint"""
response = self.client.put(
self.url_comment_detail, self.comment_data,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_delete_comment(self):
""" Test DELETE method for Comment endpoint"""
response = self.client.delete(
self.url_comment_detail, format='json'
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
PythonSeguindo os exemplos acima sabemos que ao rodar o teste o mesmo não passará, pois ainda não foi criado os endpoints de Destinations e Comments.
Desta maneira foi criado as URLs do app e adicionado na URL principal:
project_name/urls.py:
# project_name/urls.py
from django.urls import include, path
from rest_framework import routers
from tourism.api.viewsets import DestinationsViewset, CommentViewset
router = routers.DefaultRouter()
router.register(r"destinations", DestinationsViewset, basename="destinations")
router.register(r"comments", CommentViewset, basename="comments")
app_name = 'tourism'
urlpatterns = [
path('', include(router.urls)),
]
Pythonproject_name/url.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('tourism.urls')), # <------------ a inserção de tourism.urls.
]
PythonTambém sera criados os serializers e também os viewsets para que os dados sejam serializados pelo framework django-rest-framework e aceitem postagens e edições de dados em formato JSON.
tourism/api/serializers.py
from django.forms import modelformset_factory
from rest_framework import serializers
from tourism.models import Destinations, Comment
class DestinationsSerializer(serializers.ModelSerializer):
class Meta:
model = Destinations
fields = '__all__'
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
Pythontourism/api/viewsets.py:
from rest_framework import viewsets
from .serializers import DestinationsSerializer, CommentSerializer
from tourism.models import Destinations, Comment
class DestinationsViewset(viewsets.ModelViewSet):
queryset = Destinations.objects.all()
serializer_class = DestinationsSerializer
class CommentViewset(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
PythonAgora que os dados necessários foram adicionados para a serialização dos mesmos, os testes serão aceitos corretamente fazendo entender que os endpoints estão funcionando corretamente:
python manage.py test
Found 10 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..........
--------------------------------------------------------------------
Ran 10 tests in 0.061s
OK
Destroying test database for alias 'default'...
Bash