No projeto que estou trabalhando no momento, precisei lidar com uma situação que era criar modelos não-gerenciados para refletirem em tabelas já criadas no banco de dados. Até aí tudo bem, mas como poderia criar testes unitários para validar não só o modelo mas funções de negócio que iria precisar ficar atrelado a classe?
Depois de pesquisar um pouco sobre isso na documentação, encontrei uma forma de lidar com isso sem afetar o funcionamento real da aplicação. Mas como assim?
Como os unmanaged models funciona?
Todos os modelos que você cria no seu projeto Django, por padrão eles são managed, ou seja, o django irá criar a tabela no banco de dados apropriadamente, baseando no que o modelo representa. Segue um exemplo:
class Question(models.Model):
text = models.TextField()
Segue um modelo bem simples, mas que ele diz muita coisa para a ORM poder criar uma tabela como definida abaixo:
CREATE TABLE app_question(
id int not null,
text text not null,
PRIMARY KEY(id)
);
Caso for executar esse SQL em um banco que respeite o ANSI SQL, ela será criada. O django faz essa conversão por debaixo dos panos e preparar para a criação por meio das migrations. Você que conhece o framework sabe como funciona.
Agora pode ocorrer de a gestão desses objetos do banco de dados, como as tabelas, já existam ou são controladas fora do framework, por inúmeros motivos, e por isso não faz sentido o Django criá-las senão o encontrará problemas, sendo que não tem como ter tabelas duplicadas. Dessa forma o framework oferece no Meta do seu modelo a propriedade managed. Se você definir como False, ela não será responsável pela criação, alteração e remoção da tabela que o modelo se espelha.
O caso comum de uso de modelos não-gerenciáveis é quando precisa usar um banco de dados legado em que sua aplicação Django precisará acessar. O comando inspectdb do manage.py é um ótimo auxílio para fazer o parser das tabelas desse banco para classes de modelo. Se nunca usou, recomendo que faça um teste e acredito que vai ser bem útil um dia.
Classes não-gerenciáveis e os testes
Por não serem gerenciáveis pelo Django, não quer dizer que não vai poder:
- Inserir/alterar/remover registros
- Efetuar consultas nos registros
Mas como disse antes, não poderá criar/alterar/remover a tabela. Quando você faz testes automatizados no Django e isso envolver modelos, ao rodar os testes o comportamento do framework é:
- Criar tabelas no banco de testes usando as migrations (default em SQLite: test_nome_do_banco.sqlite)
- Prepara o suite de testes
- Faz o uso do método setUp
- Executa os testes automatizados
- Se for usado os modelos para criar dados fake, insere na tabela recém criada
- Faz o uso do método tearDown
- Remove os registros criados nos testes
- Elimina o suite de testes
- Remove as tabelas do banco de dados de teste
Esse passo a passo foi descrito de forma bem grosseira, mas no geral é como funciona. O problema é que no caso de modelos não-gerenciados não irá funcionar porque se for criar dados fake com o modelo, vai dar erro dizendo que a tabela não existe e que o modelo não autoriza o Django a criar a tabela. Como lidar com isso?
Uma abordagem simples
Buscando alternativas de lidar com isso encontrei uma solução que é alterar o valor do managed no teste ou criar um TestRunner para isso. Se um o outro vai ser melhor, vai depender do problema que quer resolver, mas a primeira é a mais prática.
Como assim? Digamos que temos um modelo não gerenciado que queremos testar:
class LegacyTable(models.Model):
id = models.PositiveIntegerField(primary_key=True, db_column="cod_legacy")
name = models.CharField(max_length=255, db_column="txt_name")
class Meta:
db_table = "legacy_table"
managed = False
Criando ele e rodar o makemigrations
para o app que o modelo está, irá criar uma migração parecido com essa:
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.CreateModel(
name='LegacyTable',
fields=[
('id', models.PositiveIntegerField(db_column='cod_legacy', primary_key=True, serialize=False)),
('name', models.CharField(db_column='txt_name', max_length=255)),
],
options={
'db_table': 'legacy_table',
'managed': False,
},
),
]
O que podemos fazer é definir uma variável no nosso settings.py para que caso formos rodar testes automatizados ele esteja como True:
# settings.py
# ...
DEBUG = True
TESTING = True
E com isso alteramos a migração para ficar assim:
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.CreateModel(
name='LegacyTable',
fields=[
('id', models.PositiveIntegerField(db_column='cod_legacy', primary_key=True, serialize=False)),
('name', models.CharField(db_column='txt_name', max_length=255)),
],
options={
'db_table': 'legacy_table',
'managed': settings.TESTING,
},
),
]
Dessa forma, quando rodarmos manage.py test app
a suíte de testes irá considerar como um modelo convencional, criará a tabela
no banco que será usado nos testes, e assim poderemos criar os dados fake perfeitamente. Claro que ao rodar em ambiente que não irá precisar desse tipo de situção, a propriedade TESTING
precisa estar False
.
Com isso conseguimos criar testes perfeitamente sem precisar fazer customizações malucas. Quem tiver outras soluções, coloque aqui nos comentários!
Até mais!