Criando testes de unmanaged models no Django

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!