Testes Django com TDD
Desenvolvimento orientado a testes para aplicações Django usando pytest, factory_boy e Django REST Framework.
Quando Ativar
- Escrevendo novas aplicações Django
- Implementando APIs Django REST Framework
- Testando models, views e serializers do Django
- Configurando infraestrutura de testes para projetos Django
Fluxo de Trabalho TDD para Django
Ciclo Vermelho-Verde-Refatoração
# Passo 1: VERMELHO - Escreva um teste que falha
def test_criacao_usuario():
usuario = User.objects.create_user(email='[email protected]', senha='testesenha123')
assert usuario.email == '[email protected]'
assert usuario.check_password('testesenha123')
assert not usuario.is_staff
# Passo 2: VERDE - Faça o teste passar
# Crie o model User ou a factory
# Passo 3: REFATORAR - Melhore mantendo os testes verdes
Configuração
Configuração do pytest
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = config.settings.test
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--reuse-db
--nomigrations
--cov=apps
--cov-report=html
--cov-report=term-missing
--strict-markers
markers =
slow: marca testes como lentos
integration: marca testes como de integração
Configurações de Teste
# config/settings/test.py
from .base import *
DEBUG = True
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
# Desativar migrações para ganhar velocidade
class DisableMigrations:
def __contains__(self, item):
return True
def __getitem__(self, item):
return None
MIGRATION_MODULES = DisableMigrations()
# Hash de senha mais rápido
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
# Backend de e-mail
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Celery sempre eager
CELERY_TASK_ALWAYS_EAGER = True
CELERY_TASK_EAGER_PROPAGATES = True
conftest.py
# tests/conftest.py
import pytest
from django.utils import timezone
from django.contrib.auth import get_user_model
User = get_user_model()
@pytest.fixture(autouse=True)
def configuracao_fuso_horario(settings):
"""Garantir fuso horário consistente."""
settings.TIME_ZONE = 'UTC'
@pytest.fixture
def usuario(db):
"""Criar um usuário de teste."""
return User.objects.create_user(
email='[email protected]',
password='testesenha123',
username='usuarioteste'
)
@pytest.fixture
def usuario_admin(db):
"""Criar um usuário admin."""
return User.objects.create_superuser(
email='[email protected]',
password='adminsenha123',
username='admin'
)
@pytest.fixture
def cliente_autenticado(client, usuario):
"""Retornar cliente autenticado."""
client.force_login(usuario)
return client
@pytest.fixture
def api_client():
"""Retornar cliente DRF API."""
from rest_framework.test import APIClient
return APIClient()
@pytest.fixture
def api_client_autenticado(api_client, usuario):
"""Retornar cliente API autenticado."""
api_client.force_authenticate(user=usuario)
return api_client
Factory Boy
Configuração da Factory
# tests/factories.py
import factory
from factory import fuzzy
from datetime import datetime, timedelta
from django.contrib.auth import get_user_model
from apps.produtos.models import Produto, Categoria
User = get_user_model()
class UserFactory(factory.django.DjangoModelFactory):
"""Factory para o model User."""
class Meta:
model = User
email = factory.Sequence(lambda n: f"usuario{n}@exemplo.com")
username = factory.Sequence(lambda n: f"usuario{n}")
password = factory.PostGenerationMethodCall('set_password', 'testesenha123')
first_name = factory.Faker('first_name')
last_name = factory.Faker('last_name')
is_active = True
class CategoriaFactory(factory.django.DjangoModelFactory):
"""Factory para o model Categoria."""
class Meta:
model = Categoria
nome = factory.Faker('word')
slug = factory.LazyAttribute(lambda obj: obj.nome.lower())
descricao = factory.Faker('text')
class ProdutoFactory(factory.django.DjangoModelFactory):
"""Factory para o model Produto."""
class Meta:
model = Produto
nome = factory.Faker('sentence', nb_words=3)
slug = factory.LazyAttribute(lambda obj: obj.nome.lower().replace(' ', '-'))
descricao = factory.Faker('text')
preco = fuzzy.FuzzyDecimal(10.00, 1000.00, 2)
estoque = fuzzy.FuzzyInteger(0, 100)
is_active = True
categoria = factory.SubFactory(CategoriaFactory)
criado_por = factory.SubFactory(UserFactory)
@factory.post_generation
def tags(self, create, extracted, **kwargs):
"""Adicionar tags ao produto."""
if not create:
return
if extracted:
for tag in extracted:
self.tags.add(tag)
Usando Factories
# tests/test_models.py
import pytest
from tests.factories import ProdutoFactory, UserFactory
def test_criacao_produto():
"""Testar criação de produto usando factory."""
produto = ProdutoFactory(preco=100.00, estoque=50)
assert produto.preco == 100.00
assert produto.estoque == 50
assert produto.is_active is True
def test_produto_com_tags():
"""Testar produto com tags."""
tags = [TagFactory(nome='eletrônicos'), TagFactory(nome='novo')]
produto = ProdutoFactory(tags=tags)
assert produto.tags.count() == 2
def test_multiplos_produtos():
"""Testar criação de múltiplos produtos."""
produtos = ProdutoFactory.create_batch(10)
assert len(produtos) == 10
Teste de Models
Testes de Model
# tests/test_models.py
import pytest
from django.core.exceptions import ValidationError
from tests.factories import UserFactory, ProdutoFactory
class TestUserModel:
"""Testar model User."""
def test_criar_usuario(self, db):
"""Testar criação de um usuário comum."""
usuario = UserFactory(email='[email protected]')
assert usuario.email == '[email protected]'
assert usuario.check_password('testesenha123')
assert not usuario.is_staff
assert not usuario.is_superuser
def test_criar_superusuario(self, db):
"""Testar criação de um superusuário."""
usuario = UserFactory(
email='[email protected]',
is_staff=True,
is_superuser=True
)
assert usuario.is_staff
assert usuario.is_superuser
def test_str_usuario(self, db):
"""Testar representação em string do usuário."""
usuario = UserFactory(email='[email protected]')
assert str(usuario) == '[email protected]'
class TestProdutoModel:
"""Testar model Produto."""
def test_criacao_produto(self, db):
"""Testar criação de um produto."""
produto = ProdutoFactory()
assert produto.id is not None
assert produto.is_active is True
assert produto.created_at is not None
def test_geracao_slug_produto(self, db):
"""Testar geração automática de slug."""
produto = ProdutoFactory(nome='Produto de Teste')
assert produto.slug == 'produto-de-teste'
def test_validacao_preco_produto(self, db):
"""Testar que o preço não pode ser negativo."""
produto = ProdutoFactory(preco=-10)
with pytest.raises(ValidationError):
produto.full_clean()
def test_gerente_ativo_produto(self, db):
"""Testar método ativo do gerente."""
ProdutoFactory.create_batch(5, is_active=True)
ProdutoFactory.create_batch(3, is_active=False)
contagem_ativos = Produto.objects.active().count()
assert contagem_ativos == 5
def test_gerenciamento_estoque_produto(self, db):
"""Testar gerenciamento de estoque."""
produto = ProdutoFactory(estoque=10)
produto.reduzir_estoque(5)
produto.refresh_from_db()
assert produto.estoque == 5
with pytest.raises(ValueError):
produto.reduzir_estoque(10) # Estoque insuficiente
Teste de Views
Teste de Views Django
# tests/test_views.py
import pytest
from django.urls import reverse
from tests.factories import ProdutoFactory, UserFactory
class TestProdutoViews:
"""Testar views de produto."""
def test_lista_produtos(self, client, db):
"""Testar view de listagem de produtos."""
ProdutoFactory.create_batch(10)
resposta = client.get(reverse('produtos:lista'))
assert resposta.status_code == 200
assert len(resposta.context['produtos']) == 10
def test_detalhe_produto(self, client, db):
"""Testar view de detalhe do produto."""
produto = ProdutoFactory()
resposta = client.get(reverse('produtos:detalhe', kwargs={'slug': produto.slug}))
assert resposta.status_code == 200
assert resposta.context['produto'] == produto
def test_criacao_produto_requer_login(self, client, db):
"""Testar que criação de produto requer autenticação."""
resposta = client.get(reverse('produtos:criar'))
assert resposta.status_code == 302
assert resposta.url.startswith('/accounts/login/')
def test_criacao_produto_autenticado(self, cliente_autenticado, db):
"""Testar criação de produto como usuário autenticado."""
resposta = cliente_autenticado.get(reverse('produtos:criar'))
assert resposta.status_code == 200
def test_criacao_produto_post(self, cliente_autenticado, db, categoria):
"""Testar criação de produto via POST."""
dados = {
'nome': 'Produto de Teste',
'descricao': 'Um produto de teste',
'preco': '99.99',
'estoque': 10,
'categoria': categoria.id,
}
resposta = cliente_autenticado.post(reverse('produtos:criar'), dados)
assert resposta.status_code == 302
assert Produto.objects.filter(nome='Produto de Teste').exists()
Teste de API DRF
Teste de Serializers
# tests/test_serializers.py
import pytest
from rest_framework.exceptions import ValidationError
from apps.produtos.serializers import ProdutoSerializer
from tests.factories import ProdutoFactory
class TestProdutoSerializer:
"""Testar ProdutoSerializer."""
def test_serializar_produto(self, db):
"""Testar serialização de um produto."""
produto = ProdutoFactory()
serializer = ProdutoSerializer(produto)
dados = serializer.data
assert dados['id'] == produto.id
assert dados['nome'] == produto.nome
assert dados['preco'] == str(produto.preco)
def test_desserializar_produto(self, db):
"""Testar desserialização de dados do produto."""
dados = {
'nome': 'Produto de Teste',
'descricao': 'Descrição de teste',
'preco': '99.99',
'estoque': 10,
'categoria': 1,
}
serializer = ProdutoSerializer(data=dados)
assert serializer.is_valid()
produto = serializer.save()
assert produto.nome == 'Produto de Teste'
assert float(produto.preco) == 99.99
def test_validacao_preco(self, db):
"""Testar validação de preço."""
dados = {
'nome': 'Produto de Teste',
'preco': '-10.00',
'estoque': 10,
}
serializer = ProdutoSerializer(data=dados)
assert not serializer.is_valid()
assert 'preco' in serializer.errors
def test_validacao_estoque(self, db):
"""Testar que estoque não pode ser negativo."""
dados = {
'nome': 'Produto de Teste',
'preco': '99.99',
'estoque': -5,
}
serializer = ProdutoSerializer(data=dados)
assert not serializer.is_valid()
assert 'estoque' in serializer.errors
Teste de ViewSets da API
# tests/test_api.py
import pytest
from rest_framework.test import APIClient
from rest_framework import status
from django.urls import reverse
from tests.factories import ProdutoFactory, UserFactory
class TestProdutoAPI:
"""Testar endpoints da API de Produto."""
@pytest.fixture
def api_client(self):
"""Retornar cliente API."""
return APIClient()
def test_listar_produtos(self, api_client, db):
"""Testar listagem de produtos."""
ProdutoFactory.create_batch(10)
url = reverse('api:produto-list')
resposta = api_client.get(url)
assert resposta.status_code == status.HTTP_200_OK
assert resposta.data['count'] == 10
def test_obter_produto(self, api_client, db):
"""Testar obtenção de um produto."""
produto = ProdutoFactory()
url = reverse('api:produto-detail', kwargs={'pk': produto.id})
resposta = api_client.get(url)
assert resposta.status_code == status.HTTP_200_OK
assert resposta.data['id'] == produto.id
def test_criar_produto_nao_autorizado(self, api_client, db):
"""Testar criação de produto sem autenticação."""
url = reverse('api:produto-list')
dados = {'nome': 'Produto de Teste', 'preco': '99.99'}
resposta = api_client.post(url, dados)
assert resposta.status_code == status.HTTP_401_UNAUTHORIZED
def test_criar_produto_autorizado(self, api_client_autenticado, db):
"""Testar criação de produto como usuário autenticado."""
url = reverse('api:produto-list')
dados = {
'nome': 'Produto de Teste',
'descricao': 'Teste',
'preco': '99.99',
'estoque': 10,
}
resposta = api_client_autenticado.post(url, dados)
assert resposta.status_code == status.HTTP_201_CREATED
assert resposta.data['nome'] == 'Produto de Teste'
def test_atualizar_produto(self, api_client_autenticado, db):
"""Testar atualização de um produto."""
produto = ProdutoFactory(criado_por=api_client_autenticado.usuario)
url = reverse('api:produto-detail', kwargs={'pk': produto.id})
dados = {'nome': 'Produto Atualizado'}
resposta = api_client_autenticado.patch(url, dados)
assert resposta.status_code == status.HTTP_200_OK
assert resposta.data['nome'] == 'Produto Atualizado'
def test_excluir_produto(self, api_client_autenticado, db):
"""Testar exclusão de um produto."""
produto = ProdutoFactory(criado_por=api_client_autenticado.usuario)
url = reverse('api:produto-detail', kwargs={'pk': produto.id})
resposta = api_client_autenticado.delete(url)
assert resposta.status_code == status.HTTP_204_NO_CONTENT
assert not Produto.objects.filter(id=produto.id).exists()
Boas Práticas
Estrutura de Testes
tests/
├── __init__.py
├── conftest.py # Fixtures compartilhadas
├── factories.py # Definitions de fábrica
├── test_models.py # Testes de model
├── test_views.py # Testes de view
├── test_serializers.py # Testes de serializer
├── test_api.py # Testes de API
└── test_forms.py # Testes de formulário
Padrões de Nomenclatura
- Arquivos de teste:
test_<modulo>.py - Classes de teste:
Test<nome do Modelo>ouTest<nome da View> - Métodos de teste:
test_<comportamento ou cenário> - Factories:
<Nome>Factory
Marcadores de Cobertura
import pytest
pytestmark = pytest.mark.django_db # Aplica a todos os testes no arquivo
class TestProdutoModel:
"""Testes de model para Produto."""
pytestmark = pytest.mark.unit # Aplica marcador unit a todos os testes
def test_algo_rapido(self):
"""Teste rápido - não requer banco."""
assert 1 + 1 == 2
def test_com_banco(self, db):
"""Teste que precisa do banco."""
produto = ProdutoFactory()
assert produto.id is not None
Dicas de Testes
- Teste uma coisa por vez - Cada teste deve verificar um comportamento específico
- Use nomes descritivos - Nomes dos testes devem explicar o que está sendo testado
- Mantenha os testes rápidos - Use
:memory:banco de dados e desative migrações - Isolar testes - Não compartilhe estado entre testes
- Testar casos extremos - Testes não apenas para caminho feliz, mas também para entradas inválidas
- Usar factories - Evite criar dados manualmente; use Factory Boy
- Evite lógica nos testes - Os testes devem ser simples e diretos
- Escrever testes primeiro - Siga o ciclo Vermelho-Verde-Refatoração