Testando aplicações Django

O teste automatizado é uma ferramenta extremamente útil para eliminar bugs utilizada pelo desenvolvedor Web moderno. Você pode usar uma coleção de testes – uma test suite – para resolver, ou evitar, vários problemas:

  • Quando você está escrevendo um código novo, pode usar os testes para verificar se seu código funciona como esperado.
  • Quando está refatorando ou modificando um código antigo, você pode usar os testes para garantir que suas mudanças não afetaram inesperadamente o comportamento de sua aplicação.

Testar uma aplicação Web é uma tarefa complexa, porque uma aplicação Web é feita de várias camadas de lógica – da manipulação de uma requisição em nível HTTP, para a validação e processamento de formulário, para a renderização de template. Com o framework de teste-execução do Django e outros utilitários, você pode simular requisições, inserir dados de teste, inspecionar a saída de sua aplicação e freqüentemente verificar se seu código está fazendo o que deveria.

A melhor parte é que isso tudo é muito fácil.

Este documento é dividido em duas duas seções. Na primeira, explicamos como escrever testes com Django e, posteriormente, explicamos como rodá-los.

Escrevendo testes

Existem duas maneiras de se escrever testes com Django, correspondendo com os dois frameworks de teste que estão na biblioteca padrão do Python, que são:

  • Doctests – os testes estão embutidos nas docstrings (strings de documentação) de suas funções e são escritas de tal maneira a emular uma sessão do interpretador interativo do Python. Por exemplo:

    def my_func(a_list, idx):
        """
        >>> a = ['larry', 'curly', 'moe']
        >>> my_func(a, 0)
        'larry'
        >>> my_func(a, 1)
        'curly'
        """
        return a_list[idx]
    
  • Unit tests – (Testes unitários) são testes que são expressados como métodos em uma classe Python que é uma subclasse de unittest.TestCase. Por exemplo:

    import unittest
    
    class MyFuncTestCase(unittest.TestCase):
        def testBasic(self):
            a = ['larry', 'curly', 'moe']
            self.assertEquals(my_func(a, 0), 'larry')
            self.assertEquals(my_func(a, 1), 'curly')
    

Você pode escolher o framework de teste que te agrade, dependendo da sintaxe que preferir, ou você pode misturar os dois, utilizando um framework para parte de seu código e outro framework para outra parte. Você também pode usar qualquer outro framework de testes Python, como explicaremos adiante.

Escrevendo doctests

Os doctests usam o módulo doctest padrão do Python, que procura em suas docstrings por instruções que se pareçam com uma sessão do interpretador interativo do Python. Uma explicação completa de como funciona o doctest está fora do escopo deste documento; leia a documentação oficial do Python para maiores detalhes.

O que é uma docstring?

Uma boa explicação de docstrings (e algumas diretrizes para utilizá-la de maneira completa) pode ser encontrada em PEP 257:

Uma docstring é uma literal de string que ocorre como a primeira instrução em um módulo, função, classe, ou definição de método. Esta docstring torna-se o atributo especial __doc__ do objeto em questão.

Por exemplo, esta função possui uma docstring que descreve o que ela faz:

def add_two(num):
    "Retorna o resultado da adição de dois ao número informado."
    return num + 2

Pelo motivo de que testes freqüentemente geram uma boa documentação, colocar testes diretamente nas suas docstrings é uma maneira eficiente de documentar e testar o seu código.

Para uma dada aplicação Django, o executor de testes procura por doctests em dois lugares:

  • No arquivo models.py. Você pode definir doctests para o módulo todo e/ou um doctest para cada modelo. É uma pratica comum colocar doctests de nível de aplicação numa docstring do módulo e doctests de nível de modelo em docstrings de modelos.
  • Um arquivo chamado tests.py no diretório da aplicação – ou seja, o diretório que contém models.py. Esse arquivo é um hook para todos e quaisquer doctests que você queira escrever que não sejam necessariamente relacionados a modelos.

Aqui vai um exemplo de doctest de modelo:

# models.py

from django.db import models

class Animal(models.Model):
    """
    An animal that knows how to make noise

    # Create some animals
    >>> lion = Animal.objects.create(name="lion", sound="roar")
    >>> cat = Animal.objects.create(name="cat", sound="meow")

    # Make 'em speak
    >>> lion.speak()
    'The lion says "roar"'
    >>> cat.speak()
    'The cat says "meow"'
    """
    name = models.CharField(max_length=20)
    sound = models.CharField(max_length=20)

    def speak(self):
        return 'The %s says "%s"' % (self.name, self.sound)

Quando você roda seus testes, o executor de testes irá encontrar essa docstring – note que pedaços dela parecem uma sessão interativa de Python – e executar suas linhas verificando se os resultados batem.

No caso de testes de modelo, note que o executor de testes cuida de criar seu próprio banco de dados. Ou seja, qualquer teste que acesse banco de dados – criando e gravando instâncias de modelos, por exemplo – não afetará seu banco de dados em produção. Cada doctest inicia com um banco de dados novo contendo uma tabela vazia para cada modelo. (Veja a seção de fixtures, abaixo, para mais detalhes.) Note que para usar esse recurso, o usuário que o Django utiliza para se conectar ao banco de dados deve ter direito de criar novos bancos CREATE DATABASE.

Para mais detalhes sobre como funciona o doctest, veja a documentação da biblioteca padrão para doctest

Escrevendo testes unitários

Como doctests, os testes unitários do Django usam um módulo da biblioteca padrão: unittest. Esse módulo usa uma maneira diferente de definir testes, utilizando um método baseado em classes.

Como nos doctests, para uma dada aplicação Django, o executor de testes procura por testes unitários em dois lugares:

  • O arquivo models.py. O executor de testes procura por qualquer subclasse de unittest.TestCase nesse módulo.
  • Um arquivo chamado tests.py no diretório da aplicação – o mesmo que contém models.py. Novamente, o executor de testes procura por qualquer subclasse de unittest.TestCase nesse módulo.

Este exemplo de subclasse de unittest.TestCase é equivalente ao exemplo dado na seção de doctest acima:

import unittest
from myapp.models import Animal

class AnimalTestCase(unittest.TestCase):
    def setUp(self):
        self.lion = Animal.objects.create(name="lion", sound="roar")
        self.cat = Animal.objects.create(name="cat", sound="meow")

    def testSpeaking(self):
        self.assertEquals(self.lion.speak(), 'The lion says "roar"')
        self.assertEquals(self.cat.speak(), 'The cat says "meow"')

Quando você roda seus testes, o comportamento padrão do utilitário de teste é encontrar todos os test cases (ou seja, subclasses de unittest.TestCase) nos arquivos models.py e tests.py, automaticamente montar uma test suite (conjunto de testes) destes test cases, e rodá-la.

Há uma segunda maneira de se definir um test suite para um módulo: se você define uma função chamada suite() seja em models.py ou tests.py, o executor de testes do Django usará essa função para construir a test suite para o módulo. Isso segue a organização sugerida para testes unitários. Veja a documentação do Python para mais detalhes de como construir uma test suite complexa.

Para mais detalhes sobre unittest, veja a documentação de unittest da biblioteca padrão.

Qual devo usar?

Pelo motivo de o Django suportar ambos os frameworks de testes padrão do Python, cabe a você, de acordo com seu gosto, decidir qual utilizar. Você pode até mesmo decidir usar ambos.

Para desenvolvedores novatos em testes, essa escolha pode parecer confusa. Aqui estão algumas diferenças chave para ajudá-lo a decidir qual método é melhor:

  • Se você já utiliza Python por algum tempo, o doctest provavelmente parecerá mais “pythônico”. Ele foi projetado para tornar a escrita de testes o mais fácil possível, então não requer nenhum esforço extra de escrever classes ou métodos. Você simplesmente coloca seus testes em docstrings. Isso tem a vantagem adicional de servir como documentação (e documentação correta!).

    Se você está iniciando com testes, utilizar doctests farão com que você obtenha resultados mais rapidamente.

  • O framework unittest provavelmente será bem familiar para desenvolvedores vindos do Java. O unittest é inspirado pelo JUnit do Java, então você se sentirá em casa com esse método se você já usou o JUnit ou qualquer outro framework de testes inspirado no JUnit.

  • Se você precisa escrever muitos testes que compartilham código parecido, então você vai gostar da organização baseada em classes e métodos do framework unittest. Ele facilita abstrair tarefas comuns em métodos comuns. O framework também suporta configuração explícita e/ou limpeza de rotinas, que te dão um alto nível de controle sobre o ambiente no qual seus testes são executados.

De novo, lembre-se de que você pode usar ambos os sistemas lado a lado (até mesmo em uma mesma aplicação). No final, a maioria dos projetos provavelmente acabará utilizando ambos. Cada um se destaca em diferentes circunstâncias.

Rodando os testes

Uma vez que você escreveu os testes, execute-os utilizando o utilitário do seu projeto manage.py:

$ ./manage.py test

Por padrão, isso executará cada teste em cada aplicação em INSTALLED_APPS. Se você só quer rodar os testes para uma aplicação em particular, adicione o nome da aplicação à linha de comando. Por exemplo, se seu INSTALLED_APPS contém 'myproject.polls' e 'myproject.animals', você pode rodar somente os testes unitários de myproject.animals com este comando:

# ./manage.py test animals

Note que utilizamos animals, e não myproject.animals.

Agora você pode escolher qual teste rodar.

Se você utiliza testes unitários, em vez de doctests, você pode ser ainda mais específico na escolha de quais testes executar. Para rodar um único teste em uma aplicação (por exemplo, o AnimalTestCase descrito na seção “Escrevendo testes unitários”), adicione o nome do test case à linha de comando:

$ ./manage.py test animals.AnimalTestCase

E pode ficar ainda mais granular que isso! Para rodar um único método de teste dentro de um test case, adicione o nome do método de teste:

$ ./manage.py test animals.AnimalTestCase.testFluffyAnimals

O banco de dados de teste

Testes que necessitam de uma base de dados (nomeadamente, testes de modelo) não usarão seu banco de dados “real” (produção). Um banco de dados vazio é criado em separado para os testes.

Independentemente de os testes passarem ou falharem, o banco de dados de teste é destruído quando todos os testes forem executados.

Por padrão, o nome deste banco de dados de teste é o valor da configuração DATABASE_NAME adicionando o prefixo test_. Quando um banco de dados SQLite é utilizado, os testes usarão bancos de dados na memória (ou seja, todo o banco será criado somente na memória, não utilizando nada do sistema de arquivos). Se você quiser usar um nome diferente para o banco de dados de teste, especifique o parâmetro de configuração TEST_DATABASE_NAME.

Além de utilizar um banco de dados separado, o executor de testes usará os mesmos parâmetros de banco de dados do seu arquivo de configuração: DATABASE_ENGINE, DATABASE_USER, DATABASE_HOST, etc. O banco de dados de teste é criado pelo usuário especificado em DATABASE_USER, então é necessário garantir que esta conta de usuário tem privilégios suficientes para criar um novo banco de dados no sistema.

Para um controle apurado sobre a codificação de caractere de seu banco de dados de teste, use o parâmetro de configuração TEST_DATABASE_CHARSET. Se você está utilizando MySQL, você pode também usar o parâmetro TEST_DATABASE_COLLATION para controlar uma collation particular utilizada pelo banco de dados de teste. Veja a documentação de configurações para mais detalhes dessas configurações avançadas.

Outras condições de testes

Independentemente do valor do DEBUG do seu arquivo de configuração, todos os testes do Django rodam com DEBUG=False. Isto é para assegurar que a saída observada de seu código seja igual ao da aplicação em produção.

Entendendo a saída do teste

Quando roda seus testes, você visualiza um número de mensagens à medida que o executor de testes se prepara. Você pode controlar o nível de detalhe dessas mensagens com a opção de linha de comando verbosity:

Creating test database...
Creating table myapp_animal
Creating table myapp_mineral
Loading 'initial_data' fixtures...
No fixtures found.

Isso informa que o executor de testes está criando um banco de teste, como descrito na seção anterior.

Uma vez que o banco de testes foi criado, o Django rodará os seus testes. Se tudo correr bem, você verá algo do tipo:

----------------------------------------------------------------------
Ran 22 tests in 0.221s

OK

Se existirem falhas, entretanto, você verá os detalhes completos sobre quais testes falharam:

======================================================================
FAIL: Doctest: ellington.core.throttle.models
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/dev/django/test/doctest.py", line 2153, in runTest
    raise self.failureException(self.format_failure(new.getvalue()))
AssertionError: Failed doctest test for myapp.models
  File "/dev/myapp/models.py", line 0, in models

----------------------------------------------------------------------
File "/dev/myapp/models.py", line 14, in myapp.models
Failed example:
    throttle.check("actor A", "action one", limit=2, hours=1)
Expected:
    True
Got:
    False

----------------------------------------------------------------------
Ran 2 tests in 0.048s

FAILED (failures=1)

Uma explicação completa sobre essa saída de erro está fora do escopo deste documento, mas ela é bem intuitiva. Você pode consultar a documentação da biblioteca unittest do Python para maiores detalhes.

Note que o código retornado pelo script executor de testes é o número total de testes que falharam. Se todos os testes passarem, o código de retorno é 0. Este recurso é útil se você está usando script executor de testes em um script shell e precisa verificar o sucesso ou falha naquele nível.

Ferramentas de teste

O Django provê um pequeno conjunto de ferramentas que são uma verdadeira mão- na-roda na hora de escrever os testes.

O cliente de teste

O cliente de teste é uma classe Python que age como um navegador Web, permitindo a você testar suas views e interagir com suas aplicações feitas em Django programaticamente.

Algumas das coisas que você pode fazer com o cliente de teste são:

  • Simular requisições GET e POST em uma URL e observar a resposta – tudo desde o nível baixo do HTTP (os cabeçalhos de resultado e códigos de status) até o conteúdo da página.
  • Testar se a view correta é executada para uma dada URL.
  • Testar que uma dada requisição é gerada por um determinado template, com um determinado contexto de template que contém certos valores.

Note que este cliente de teste não tem o propósito de ser um substituto para Twill, Selenium, ou outros frameworks “in-browser”. O cliente de teste do Django tem um foco diferente. Em resumo:

  • Use o cliente de teste do Django para atestar que a view correta está sendo chamada e que a view está recebendo os dados de contexto corretos.
  • Use frameworks in-browser como Twill e Selenium para testar HTML gerados e comportamento de páginas Web, ou seja, funcionalidade JavaScript.

Uma test suite completa deve usar uma combinação de ambos tipos de testes.

Visão geral e um exemplo rápido

Para usar o cliente de teste, instancie django.test.client.Client e acesse páginas Web:

>>> from django.test.client import Client
>>> c = Client()
>>> response = c.post('/login/', {'username': 'john', 'password': 'smith'})
>>> response.status_code
200
>>> response = c.get('/customer/details/')
>>> response.content
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 ...'

Como esse exemplo sugere, você pode instanciar Client de dentro de uma sessão do interpretador interativo do Python.

Note algumas coisas importantes sobre como o cliente de teste funciona:

  • O cliente de teste não requer que o servidor Web esteja rodando. Aliás, ele rodará muito bem sem nenhum servidor Web! Isso se deve ao fato de que ele evita o esforço extra do HTTP e lida diretamente com o framework Django. Isso faz com que os testes unitários rodem bem mais rápido.

  • Ao acessar as páginas, lembre-se de especificar o caminho da URL, e não todo o domínio. Por exemplo, isto é correto:

    >>> c.get('/login/')
    

    E isto incorreto:

    >>> c.get('http://www.example.com/login/')
    

    O cliente de testes não é capaz de acessar páginas Web que não fazem parte do seu projeto Django. Se você precisa acessar outras páginas Web, utilize algum módulo da biblioteca padrão do Python como urllib ou urllib2.

  • Para resolver URLs, o cliente de testes usa o URLconf que está especificado no parâmetro de configuração ROOT_URLCONF.

  • Apesar de o exemplo acima funcionar no interpretador interativo do Python, algumas funcionalidades do cliente de teste, notadamente as relacionadas a templates, somente estão disponíveis enquanto os testes estão sendo rodados.

    O motivo disso é porque o executor de testes do Django faz um pouco de magia negra para determinar qual template foi carregado por uma determinada view. Essa magia negra (essencialmente um patch no sistema de templates na memória) acontece somente durante a execução dos testes.

Fazendo requisições

Use a classe django.test.client.Client para fazer as requisições. Ela não requer argumentos em sua contrução:

class Client

Uma vez que você tenha uma instância de Client, você pode chamar qualquer um dos seguintes métodos:

get(path, data={}, **extra)

Faz uma requisição GET no path informado e devolve um objeto Response, que está documentado abaixo.

Os pares de chave-valor no dicionário data são usados para criar os dados que serão enviados via GET. Por exemplo:

>>> c = Client()
>>> c.get('/customers/details/', {'name': 'fred', 'age': 7})

...resultará em uma requisição GET equivalente a:

/customers/details/?name=fred&age=7

The extra keyword arguments parameter can be used to specify headers to be sent in the request. For example:

>>> c = Client()
>>> c.get('/customers/details/', {'name': 'fred', 'age': 7},
...       HTTP_X_REQUESTED_WITH='XMLHttpRequest')

...will send the HTTP header HTTP_X_REQUESTED_WITH to the details view, which is a good way to test code paths that use the django.http.HttpRequest.is_ajax() method.

post(path, data={}, content_type=MULTIPART_CONTENT, **extra)

Faz uma requisição POST no path informado e devolve um objeto Response, que está documentado abaixo.

Os pares de chave-valor no dicionário data são usados para enviar os dados via POST. Por exemplo:

>>> c = Client()
>>> c.post('/login/', {'name': 'fred', 'passwd': 'secret'})

...resultará em uma requisição POST a esta URL:

/login/

...com estes dados de POST:

name=fred&passwd=secret

Se você informa o content_type (ex: text/xml para um XML), o conteúdo de data será enviado como está na requisição POST, utilizando content_type no cabeçalho HTTP Content-Type.

Se você não informa um valor para content_type, os valores em data serão transmitidos como um tipo de conteúdo multipart/form-data. Nesse caso, os pares chave-valor em data serão codificados como uma mensagem multipart e usados para criar os dados de POST.

Para enviar múltiplos valores para uma chave – por exemplo, para especificar as seleções para um <select multiple> – informe os valores como uma lista ou tupla para a chave. Por exemplo, este valor de data enviará três valores selecionados para o campo de nome choices:

{'choices': ('a', 'b', 'd')}

Enviar arquivos é um caso especial. Para postar um arquivo, você só precisa informar o nome do campo como chave, e um manipulador de arquivo apontando para o arquivo que deseja postar como valor. Por exemplo:

>>> c = Client()
>>> f = open('wishlist.doc')
>>> c.post('/customers/wishes/', {'name': 'fred', 'attachment': f})
>>> f.close()

(O nome attachment aqui não é relevante; use qualquer nome que o seu código de processamento de arquivo espera.)

Perceba que você deve fechar o arquivo manualmente depois que ele foi informado para o post().

The extra argument acts the same as for Client.get().

login(**credentials)

Se seu site Django usa o sistema de autenticação e você precisa logar usuários, você pode usar o método login() do cliente de testes para simular o efeito de um usuário logando no site.

Depois de chamar este método, o cliente de testes terá todos os cookies e dados de sessão necessários para passar a qualquer teste baseado em login que faça parte de uma view.

O formato do argumento credentials depende de qual backend de autenticação você está usando (que é configurado pelo parâmetro de configuração AUTHENTICATION_BACKENDS). Se você está usando o backend padrão de autenticação do Django (ModelBackend), credentials deve ser o nome do usuário e a senha, informados como argumentos nomeados:

>>> c = Client()
>>> c.login(username='fred', password='secret')
>>> # Agora você pode acessar a view que somente está disponível
>>> # para usuários logados.

Se você está usando um backend de autenticação diferente, este método requer credenciais diferentes. Ele requer as mesmas credenciais que o método authenticate() do backend.

login() devolve True se as credenciais foram aceitas e o login ocorreu com sucesso.

Finalmente, você precisa lembrar-se de criar contas de usuários antes de utilizar este método. Como explicado acima, o executor de testes utiliza uma base de dados de teste, que não contém usuários criados. Como resultado, contas que são válidas no seu site em produção não funcionarão nas condições de teste. Você precisa criar os usuários como parte de sua test suite – tanto manualmente (utilizando a API de modelos do Django) ou com uma test fixture.

Lembre-se que se você quiser que seu usuários de teste tenha uma senha, você não pode setar a senha do usuário usando o atributo password diretamente – você deve usar a função set_password() para armazenar corretamente uma senha criptografada. Alternativamente, você pode usar o método helper create_user() para criar um novo usuário com a senha criptografada corretamente.

logout()

Se o seu site usa o sistema de autenticação do Django, o método logout() pode ser utilizado para simular o efeito de um usuário se deslogar do seu site.

Depois de chamar este método, o cliente de teste terá todos os cookies e dados de sessão retornados para os valores padrões. Requisições subseqüentes parecerão vir de um usuário anônimo AnonymousUser.

Testando as respostas

Ambos os métodos get() e post() devolvem um objeto Response. Este objeto Response não é o mesmo que o objeto HttpResponse retornado pelas views do Django; o objeto de resposta do teste tem dados adicionais úteis para serem verificados pelo código de teste.

Especificamente, o objeto Response tem os seguintes atributos:

class Response
client

O cliente de teste que foi usado para fazer a requisição que resultou na resposta.

content

O corpo da resposta, como uma string. Este é o conteúdo final da página que foi gerada pela view, ou alguma mensagem de erro.

context

A instância Context que foi utilizada pelo template para produzir o conteúdo da resposta.

Se a página gerada utilizou múltiplos templates, então o context será uma lista de objetos Context, na ordem em que foram utilizados.

request

Os dados da requisição que provocaram a resposta.

status_code

O status da resposta HTTP, como um inteiro. Veja RFC2616 para uma lista completa de códigos de status HTTP.

template

A instância de Template que foi utilizada para gerar o conteúdo final. Use template.name para obter o nome do arquivo do template, se o template foi carregado de um arquivo. (O nome é uma string como 'admin/index.html'.)

Se a página gerada utilizou vários templates – ex: utilizando herança de templates – então template será uma lista de instâncias de Template, na ordem em que eles foram utilizados.

Você pode também usar uma sintaxe de dicionário no objeto de resposta para obter o valor de qualquer configuração nos cabeçalhos HTTP. Por exemplo, você pode determinar o conteúdo de uma resposta usando response['Content-Type'].

Exceções

Se você aponta o cliente de testes para uma view que lança uma exceção, esta exceção estará visível no test case. Você pode então usar blocos try...catch ou unittest.TestCase.assertRaises() para testar por exceções.

As únicas exceções que não estão visíveis ao cliente de teste são Http404, PermissionDenied e SystemExit. O Django captura estas exceções internamente e converte-as em códigos de resposta HTTP adequados. Nesses casos, você pode verificar response.status_code no seu código.

Estado persistente (persistent state)

O cliente de teste é “stateful”, ou seja, mantém o estado. Se uma resposta retorna um cookie, então esse cookie será armazenado no cliente de teste e enviado com todas as requisições subseqüentes de get() e post().

Políticas de expiração desses cookies não são seguidas. Se você quiser que um cookie expire, delete-o manualmente ou crie uma nova instância de Client (o que irá deletar todos os cookies).

Um cliente de teste possui dois atributos que armazenam informações de estado persistente. Você pode acessar essas propriedades como parte de uma condição de teste.

Client.cookies

Um objeto SimpleCookie Python, contendo os valores atuais de todos os cookies do cliente. Veja a Documentação do módulo cookie para saber mais.

Client.session

A dictionary-like object containing session information. See the session documentation for full details. Um objeto que se comporta como dicionário contendo informações da sessão. Veja a documentação da sessão para maiores detalhes.

Exemplo

Segue um teste unitário simples usando o cliente de teste:

import unittest
from django.test.client import Client

class SimpleTest(unittest.TestCase):
    def setUp(self):
        # Cada teste precisa de um cliente.
        self.client = Client()

    def test_details(self):
        # Faz uma requisição GET.
        response = self.client.get('/customer/details/')

        # Verifica se a resposta foi 200 OK.
        self.failUnlessEqual(response.status_code, 200)

        # Verifica se o contexto utilizado contém 5 customers.
        self.failUnlessEqual(len(response.context['customers']), 5)

TestCase

Uma classe de teste unitário normal do Python extende uma classe base unittest.TestCase. O Djagno provê uma extensão desta classe:

class TestCase

Esta classe provê algumas capacidades adicionais qeu podem ser úteis para testar site Web.

Converter um unittest.TestCase normal para um Django TestCase é fácil: basta mudar a classe base de seu teste de unittest.TestCase para django.test.TestCase. Todas as funcionalidades padrões de teste unitário do Python continuarão disponíveis, mas serão aumentadas com algumas adições úteis.

O cliente de teste default

TestCase.client

Cada teste em uma instância de django.test.TestCase tem acesso a uma instância do cliente de testes do Django. Esse cliente pode ser acessado como self.client. O cliente é recriado para cada teste, então você não tem de se preocupar sobre o estado (como os cookies) ser levado de um teste a outro.

Isso significa que, em vez de instanciar um Client em cada teste:

import unittest
from django.test.client import Client

class SimpleTest(unittest.TestCase):
    def test_details(self):
        client = Client()
        response = client.get('/customer/details/')
        self.failUnlessEqual(response.status_code, 200)

    def test_index(self):
        client = Client()
        response = client.get('/customer/index/')
        self.failUnlessEqual(response.status_code, 200)

...você só precisa se referir a self.client, como:

from django.test import TestCase

class SimpleTest(TestCase):
    def test_details(self):
        response = self.client.get('/customer/details/')
        self.failUnlessEqual(response.status_code, 200)

    def test_index(self):
        response = self.client.get('/customer/index/')
        self.failUnlessEqual(response.status_code, 200)

Carga de fixture

TestCase.fixtures

Um teste para um site Web com banco de dados não tem muita utilidade se não existir dados no banco. Para facilitar a carga destes dados no banco, a classe TestCase do Django proporciona uma maneira de carregar fixtures.

Uma fixture é uma coleção de dados que o Django sabe como importar para um banco de dados. Por exemplo, se o seu site tem contas de usuários, você pode configurar uma fixture de usuários no intuito de popular seu banco durante os testes.

O método mais simples e direto de criar uma fixture é usar o comando manage.py dumpdata. Isso assume que você já tem algum dado em seu banco. Veja a documentação do dumpdata para mais detalhes.

Note

Se você já rodou alguma vez o manage.py syncdb, você já usou fixture sem nem mesmo saber! Quando você dá um syncdb no banco de dados pela primeira vez, o Django instala uma fixture chamada initial_data. Isso dá a possibilidade de popular um banco de dados novo com qualquer dado relacional, como um conjunto padrão de categorias, por exemplo.

Fixtures com outros nomes podem sempre ser instaladas manualmente usando o comando manage.py loaddata.

Uma vez que você criou uma fixture e colocou em algum lugar em seu projeto Django, você pode utilizá-la nos seus testes unitários especificando um atributo de classe fixtures na sua subclasse de django.test.TestCase:

from django.test import TestCase
from myapp.models import Animal

class AnimalTestCase(TestCase):
    fixtures = ['mammals.json', 'birds']

    def setUp(self):
        # Definições de testes como anteriormente.

    def testFluffyAnimals(self):
        # Um teste que usa fixtures.

Eis o que acontecerá nesse caso:

  • No início de cada test case, antes de setUp() ser rodado, o Django limpará o banco de dados, retornando um banco de dados no estado que estava diretamente após a chamada de syncdb.
  • Em seguida, todas as fixtures nomeadas são instaladas. Nesse exemplo, o Django instalará qualquer fixture JSON chamada mammals, seguida por qualquer fixture chamada birds. Veja a documentação do loaddata para mais detalhes sobre a definição e a instalação de fixtures.

Esse procedimento de limpeza/carga é repetido para cada teste no test case, então você pode ter certeza de que o resultado de um teste não será afetado por outro teste, ou pela ordem de execução dos testes.

Configuração do URLconf

TestCase.urls

Se a sua aplicação possui views, você pode querer incluir testes que utilizam o cliente de testes para exercitá-las. Entretanto, um usuário final é livre para configurar as views em sua aplicação em qualquer URL de sua preferência. Isso significa que seus testes não podem contar com o fato de que suas views estarão disponíveis em uma URL em particular.

A fim de proporcionar URLs confiáveis para seu teste, django.test.TestCase tem a capacidade de customizar a configuração do URLconf pelo período de execução dos testes. Se sua instância de TestCase define o atributo urls, o TestCase usará os valores deste atributo como o ROOT_URLCONF pelo período de execução do teste.

Por exemplo:

from django.test import TestCase

class TestMyViews(TestCase):
    urls = 'myapp.test_urls'

    def testIndexPageView(self):
        # Aqui você testará sua view usando ``Client``.

Esse test case utilizará o conteúdo de myapp.test_urls como o URLconf durante a execução do teste.

Esvaziando a caixa de saída de teste

Se você usa a classe Django TestCase, o executor de testes limpará o conteúdo da caixa de saída de e-mail no início de cada teste.

Para mais detalhes sobre os serviços de e-mail durante os teste, veja Serviços de e-mail.

Assertions

Como numa classe Python unittest.TestCase normal que implementa métodos de asserção como assertTrue e assertEquals, a classe customizada TestCase do Django disponibiliza alguns métodos de asserção customizados que são úteis para testar aplicações Web:

TestCase.assertContains(response, text, count=None, status_code=200)

Testa se uma instância de Response produziu o status_code informado e que o text aparece no conteúdo da resposta. Se count é fornecido, text deve ocorrer exatamente count vezes na resposta.

TestCase.assertNotContains(response, text, status_code=200)

Testa se uma instância de Response produziu o status_code informado e que o text não aparece no conteúdo da resposta.

TestCase.assertFormError(response, form, field, errors)

Testa se um campo no formulário lança a lista de erros fornecida quando gerado no formulário.

form é o nome da instância de Form informada ao contexto do template.

field é o nome do campo no formulário para verificar. Se field tem um valor de None, erros não relacionados a campos (erros que você pode acessar via form.non_field_errors()) serão verificados.

errors é uma string de erro, ou uma lista de strings de erro, que são esperados como resultado da validação do formulário.

TestCase.assertTemplateUsed(response, template_name)
Testa se o template com o nome informado foi usado na geração da resposta.

O nome é uma string como 'admin/index.html'.

TestCase.assertTemplateNotUsed(response, template_name)

Testa se o template com o nome informado não foi usado na geração da resposta.

TestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200)

Teste se a resposta devolve um status de redirecionamento status_code, se redirecionou para a URL expected_url (incluindo quaisquer dados GET), e a página subseqüente foi recebida com o target_status_code.

Serviços de e-mail

Se alguma de suas views manda e-mail usando a Funcionalidade de e-mail do Django, você provavelmente não quer mandar e-mail cada vez que você roda um teste utilizando a view. Por esse motivo, o executor de testes do Django automaticamente redireciona todos e-mails enviados por meio do Django para uma caixa de saída fictícia. Isso deixa você testar cada aspecto do envio de e-mail – do númeto de mensagens enviadas ao conteúdo de cada mensagem – sem ter de enviar as mensagens de verdade.

O executor de testes consegue fazer isso de forma transparente trocando a classe <~django.core.mail.SMTPConnection> normal por uma versão diferente. (Não se preocupe – isso não tem efeito em quaisquer outros meios de envio de e-mail fora do Django, como o servidor de e-mail de sua máquina, se estiver rodando um).

django.core.mail.outbox

Durante a execução dos testes, cada e-mail de saída é gravado em django.core.mail.outbox. Esta é uma lista simples de todas as instâncias de <~django.core.mail.EmailMessage> que foram enviadas. Ela não existem nas condições normais de execução, ou seja, quando você não está rodando testes unitários. A caixa de saída é criada durante a configuração do teste, junto com a <~django.core.mail.SMTPConnection> fictícia. Quando o framework de teste é encerrado, a classe <~django.core.mail.SMTPConnection> padrão é restaurada, e a caixa de saída de teste é destruída.

The outbox attribute is a special attribute that is created only when the tests are run. It doesn’t normally exist as part of the django.core.mail module and you can’t import it directly. The code below shows how to access this attribute correctly.

Aqui vai um exemplo de teste que verifica o tamanho e conteúdo de django.core.mail.outbox:

from django.core import mail
from django.test import TestCase

class EmailTest(TestCase):
    def test_send_email(self):
        # Envia mensagem.
        mail.send_mail('Assunto aqui', 'Aqui vai a mensagem.',
            'from@example.com', ['to@example.com'],
            fail_silently=False)

        # Verifica se uma mensagem foi enviada.
        self.assertEqual(len(mail.outbox), 1)

        # Verifica se o assunto da mensagem é igual
        self.assertEqual(mail.outbox[0].subject, 'Assunto aqui')

Como dito anteriormente, a caixa de saída de teste é esvaziada no início de cada teste em um TestCase Django. Para esvaziar a caixa de saída manualmente, atribua uma lista vazia para mail.outbox:

from django.core import mail

# Esvazia a caixa de saída
mail.outbox = []

Utilizando frameworks de testes diferentes

Obviamente, doctest e unittest não são os únicos frameworks de testes Python. Apesar de o Django não suportar explicitamente frameworks alternativos, ele provê uma maneira de invocar testes construídos para um framework alternativo como se fossem testes Django normais.

Quando você roda ./manage.py test, o Django procura a configuração TEST_RUNNER para determinar o que fazer. Por padrão, o TEST_RUNNER aponta para 'django.test.simple.run_tests'. Este método define o comportamento padrão dos testes no Django. Esse comportamento envolve:

  1. Fazer uma configuração global antes do teste.
  2. Criar o banco de dados de teste.
  3. Executar syncdb para instalar os modelos e dados iniciais no banco de dados de teste.
  4. Procurar testes unitários e doctests nos arquivos models.py e tests.py em cada aplicação instalada.
  5. Rodar os testes unitários e doctests que forem encontrados.
  6. Destruir o banco de dados de teste.
  7. Executar um encerramento global após o término dos testes.

Se você define seu próprio método executor de testes e aponta o TEST_RUNNER para este método, o Django rodará o seu executor de testes toda vez que você rodar ./manage.py test. Desta maneira, é possível usar qualquer framework de testes que possa ser executado a partir de um código Python.

Definindo o executor de testes

Por convenção, um executor de testes deve ser chamado run_tests. O único requisito é que ele tenha os mesmos argumentos que o executor de testes do Django:

run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[])

test_labels é uma lista de strings descrevendo os testes a serem rodados. Cada string pode ter uma das três formas a seguir:

  • app.TestCase.test_method – Executa um único método de teste em um test case.
  • app.TestCase – Executa todos os métodos de teste em um test case.
  • app – Procura e roda todos os testes na aplicação nomeada.

Se test_labels tem o valor None, o executor de testes deve procurar e rodar os testes para todas as aplicações em INSTALLED_APPS.

verbosity determina a quantidade de notificações e informações de debug que serão apresentadas no console; 0 significa sem saída, 1 é saída normal, e 2 é saída detalhada.

Se interactive é True, a test suite tem permissão para pedir ao usuário por instruções quando é executada. Um exemplo desse comportamento seria pedir permissão para excluir um teste existente do banco de dados. Se interactive é False, a test suite deve ser capaz de rodar sozinha, sem intervenção manual.

extra_tests é uma lista de instâncias extra de TestCase para adicionar à suite, que é rodada pelo executor de testes. Esses testes extras são executados além dos descobertos nos módulos listados em module_list.

Este método deve retornar o número de testes que falharam.

Utilitários de testes

Para ajudar na criação de seu próprio executor de testes, o Django possui alguns métodos utilitários no módulo django.test.utils.

setup_test_environment()

Executa quaisquer configurações prévias aos testes, como a instalação de instrumentação para o sistema de templates e a configuração do SMTPConnection fictício.

teardown_test_environment()

Executa quaisquer tarefas globais de encerramento após o término dos testes, como remover a magia negra do sistema de templates e restaurar os serviços normais de e-mail.

The creation module of the database backend (connection.creation) also provides some utilities that can be useful during testing.

create_test_db(verbosity=1, autoclobber=False)

Cria um novo banco de dados e roda o syncdb nele.

verbosity tem o mesmo comportamento que em run_tests().

autoclobber descreve o comportamento que acontecerá se um banco de dados com o mesmo nome que o banco de teste é encontrado:

  • Se autoclobber é False, será pedido ao usuário para aprovar a destruição do banco de dados existente. sys.exit é chamado se o usuário não aprovar.
  • Se autoclobber é True, o banco de dados será destruído sem consultar o usuário.

Returns the name of the test database that it created.

create_test_db() tem o efeito colateral de modificar settings.DATABASE_NAME para bater com o nome do banco de dados de teste.

create_test_db() agora retorna o nome do banco de dados de teste.
destroy_test_db(old_database_name, verbosity=1)

Destrói o banco de dados cujo nome está no parâmetro de configuração DATABASE_NAME e restaura o valor de DATABASE_NAME para o nome fornecido.

verbosity tem o mesmo comportamento que em run_tests().