testes de unidade do django sem um db


126

Existe a possibilidade de escrever unittests do django sem configurar um db? Quero testar a lógica de negócios que não requer a instalação do banco de dados. E embora seja rápido configurar um banco de dados, eu realmente não preciso disso em algumas situações.


Eu estou querendo saber se isso realmente importa. O banco de dados é mantido na memória + se você não possui nenhum modelo, nada é executado com o banco de dados. Portanto, se você não precisar, não configure modelos.
Torsten Engelbrecht

3
Eu tenho modelos, mas para esses testes eles não são relevantes. E o db não é mantido na memória, mas construído no mysql, no entanto, especificamente para esse fim. Não que eu queira isso. Talvez eu possa configurar o django para usar um db na memória para teste. Você sabe como fazer isso?
Paweloque #

Me desculpe. Os bancos de dados na memória são exatamente o caso quando você usa um banco de dados SQLite. Exceto isso, não vejo uma maneira de evitar a criação do teste db. Não há nada sobre isso nos documentos + nunca senti a necessidade de evitá-lo.
Torsten Engelbrecht

3
A resposta aceita não me funcionou. Em vez disso, isso funcionou perfeitamente: caktusgroup.com/blog/2013/10/02/skipping-test-db-creation
Hugo Pineda

Respostas:


122

Você pode subclassificar DjangoTestSuiteRunner e substituir os métodos setup_databases e teardown_databases a serem aprovados.

Crie um novo arquivo de configurações e defina TEST_RUNNER para a nova classe que você acabou de criar. Então, quando você estiver executando seu teste, especifique seu novo arquivo de configurações com o sinalizador --settings.

Aqui está o que eu fiz:

Crie um corredor de traje de teste personalizado semelhante a este:

from django.test.simple import DjangoTestSuiteRunner

class NoDbTestRunner(DjangoTestSuiteRunner):
  """ A test runner to test without database creation """

  def setup_databases(self, **kwargs):
    """ Override the database creation defined in parent class """
    pass

  def teardown_databases(self, old_config, **kwargs):
    """ Override the database teardown defined in parent class """
    pass

Crie configurações personalizadas:

from mysite.settings import *

# Test runner with no database creation
TEST_RUNNER = 'mysite.scripts.testrunner.NoDbTestRunner'

Quando você estiver executando seus testes, execute-o da seguinte maneira com o sinalizador --settings definido no seu novo arquivo de configurações:

python manage.py test myapp --settings='no_db_settings'

ATUALIZAÇÃO: abril / 2018

Desde o Django 1.8, o módulo foi movido para .django.test.simple.DjangoTestSuiteRunner 'django.test.runner.DiscoverRunner'

Para mais informações, consulte a seção oficial do documento sobre corredores de teste personalizados.


2
Este erro ocorre quando você tem testes que precisam de transações no banco de dados. Obviamente, se você não tiver um banco de dados, não poderá executar esses testes. Você deve executar seus testes separadamente. Se você acabou de executar seu teste usando python manage.py test --settings = new_settings.py, ele executará vários outros testes de outros aplicativos que podem exigir banco de dados.
mohi666

5
Observe que você precisará estender o SimpleTestCase em vez do TestCase para suas classes de teste. O TestCase espera um banco de dados.
Ben Roberts

9
Se você não quiser usar um novo arquivo de configurações, poderá especificar o novo TestRunner na linha de comando com a --testrunneropção
quer

26
Ótima resposta!! No django 1.8, de django.test.simple import O DjangoTestSuiteRunner foi alterado para django.test.runner import DiscoverRunner Espero que ajude alguém!
Josh Brown

2
No Django 1.8 e acima, uma pequena correção no código acima pode ser feita. A instrução de importação pode ser alterada para: from django.test.runner import DiscoverRunner O NoDbTestRunner agora deve estender a classe DiscoverRunner.
Aditya Satyavada

77

Geralmente os testes em um aplicativo podem ser classificados em duas categorias

  1. Testes de unidade, testam trechos de código individuais em isolamento e não precisam ir para o banco de dados
  2. Casos de teste de integração que realmente vão para o banco de dados e testam a lógica totalmente integrada.

O Django suporta testes de unidade e integração.

Os testes de unidade não precisam configurar e desmontar o banco de dados e estes devemos herdar do SimpleTestCase .

from django.test import SimpleTestCase


class ExampleUnitTest(SimpleTestCase):
    def test_something_works(self):
        self.assertTrue(True)

Para casos de teste de integração herdados do TestCase, por sua vez, herda do TransactionTestCase e ele irá configurar e derrubar o banco de dados antes de executar cada teste.

from django.test import TestCase


class ExampleIntegrationTest(TestCase):
    def test_something_works(self):
        #do something with database
        self.assertTrue(True)

Essa estratégia garantirá que o banco de dados seja criado e destruído apenas para os casos de teste que acessam o banco de dados e, portanto, os testes serão mais eficientes


37
Isso pode tornar os testes em execução mais eficientes, mas observe que o executor de teste ainda cria bancos de dados de teste na inicialização.
Monkut

6
Muito mais simples que a resposta escolhida. Muito obrigado!
KFunk

1
@monkut Não ... se você tiver apenas a classe SimpleTestCase, o executor de teste não executará nada, consulte este projeto .
Claudio Santos

O Django ainda tentará criar um banco de dados de teste, mesmo se você usar o SimpleTestCase. Veja esta pergunta .
Marko Prcać 22/08/19

o uso do SimpleTestCase funciona exatamente para testar métodos utilitários ou trechos e não usa ou cria o banco de dados de teste. Exatamente o que eu preciso!
precisa

28

De django.test.simple

  warnings.warn(
      "The django.test.simple module and DjangoTestSuiteRunner are deprecated; "
      "use django.test.runner.DiscoverRunner instead.",
      RemovedInDjango18Warning)

Então substitua em DiscoverRunnervez de DjangoTestSuiteRunner.

 from django.test.runner import DiscoverRunner

 class NoDbTestRunner(DiscoverRunner):
   """ A test runner to test without database creation/deletion """

   def setup_databases(self, **kwargs):
     pass

   def teardown_databases(self, old_config, **kwargs):
     pass

Use assim:

python manage.py test app --testrunner=app.filename.NoDbTestRunner

8

Eu escolhi herdar django.test.runner.DiscoverRunnere fazer algumas adições ao run_testsmétodo.

Minha primeira adição verifica se é necessário configurar um banco de dados e permite que a setup_databasesfuncionalidade normal seja ativada se um banco de dados for necessário. Minha segunda adição permite que o normal teardown_databasesseja executado se o setup_databasesmétodo tiver permissão para ser executado.

Meu código pressupõe que qualquer TestCase que herda django.test.TransactionTestCase(e, portanto django.test.TestCase) exige que um banco de dados seja configurado. Eu fiz essa suposição porque os documentos do Django dizem:

Se você precisar de outros recursos específicos do Django, mais complexos e pesados, como ... Testando ou usando o ORM ..., você deve usar TransactionTestCase ou TestCase.

https://docs.djangoproject.com/en/1.6/topics/testing/tools/#django.test.SimpleTestCase

mysite / scripts / settings.py

from django.test import TransactionTestCase     
from django.test.runner import DiscoverRunner


class MyDiscoverRunner(DiscoverRunner):
    def run_tests(self, test_labels, extra_tests=None, **kwargs):
        """
        Run the unit tests for all the test labels in the provided list.

        Test labels should be dotted Python paths to test modules, test
        classes, or test methods.

        A list of 'extra' tests may also be provided; these tests
        will be added to the test suite.

        If any of the tests in the test suite inherit from
        ``django.test.TransactionTestCase``, databases will be setup. 
        Otherwise, databases will not be set up.

        Returns the number of tests that failed.
        """
        self.setup_test_environment()
        suite = self.build_suite(test_labels, extra_tests)
        # ----------------- First Addition --------------
        need_databases = any(isinstance(test_case, TransactionTestCase) 
                             for test_case in suite)
        old_config = None
        if need_databases:
        # --------------- End First Addition ------------
            old_config = self.setup_databases()
        result = self.run_suite(suite)
        # ----------------- Second Addition -------------
        if need_databases:
        # --------------- End Second Addition -----------
            self.teardown_databases(old_config)
        self.teardown_test_environment()
        return self.suite_result(suite, result)

Por fim, adicionei a seguinte linha ao arquivo settings.py do meu projeto.

mysite / settings.py

TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner'

Agora, ao executar apenas testes não dependentes de banco de dados, minha suíte de testes executa uma ordem de magnitude mais rapidamente! :)


6

Atualizado: veja também esta resposta para usar uma ferramenta de terceiros pytest.


@ Cesar está certo. Após a execução acidental ./manage.py test --settings=no_db_settings, sem especificar o nome de um aplicativo, meu banco de dados de desenvolvimento foi eliminado.

Para uma maneira mais segura, use o mesmo NoDbTestRunner, mas em conjunto com o seguinte mysite/no_db_settings.py:

from mysite.settings import *

# Test runner with no database creation
TEST_RUNNER = 'mysite.scripts.testrunner.NoDbTestRunner'

# Use an alternative database as a safeguard against accidents
DATABASES['default']['NAME'] = '_test_mysite_db'

Você precisa criar um banco de dados chamado _test_mysite_dbusando uma ferramenta de banco de dados externo. Em seguida, execute o seguinte comando para criar as tabelas correspondentes:

./manage.py syncdb --settings=mysite.no_db_settings

Se você estiver usando o Sul, execute também o seguinte comando:

./manage.py migrate --settings=mysite.no_db_settings

ESTÁ BEM!

Agora você pode executar testes de unidade incrivelmente rápidos (e seguros):

./manage.py test myapp --settings=mysite.no_db_settings

Eu executei testes usando pytest (com o plug-in pytest-django) e NoDbTestRunner, se de alguma forma você criar um objeto acidentalmente em um testcase e não substituir o nome do banco de dados, o objeto será criado nos bancos de dados locais configurados no diretório configurações. O nome 'NoDbTestRunner' deve ser 'NoTestDbTestRunner' porque não criará o banco de dados de teste, mas usará o banco de dados nas configurações.
Gabriel Muj

2

Como alternativa para modificar suas configurações para tornar o NoDbTestRunner "seguro", aqui está uma versão modificada do NoDbTestRunner que fecha a conexão atual do banco de dados e remove as informações de conexão das configurações e do objeto de conexão. Funciona para mim, teste-o no seu ambiente antes de confiar nele :)

class NoDbTestRunner(DjangoTestSuiteRunner):
    """ A test runner to test without database creation """

    def __init__(self, *args, **kwargs):
        # hide/disconnect databases to prevent tests that 
        # *do* require a database which accidentally get 
        # run from altering your data
        from django.db import connections
        from django.conf import settings
        connections.databases = settings.DATABASES = {}
        connections._connections['default'].close()
        del connections._connections['default']
        super(NoDbTestRunner,self).__init__(*args,**kwargs)

    def setup_databases(self, **kwargs):
        """ Override the database creation defined in parent class """
        pass

    def teardown_databases(self, old_config, **kwargs):
        """ Override the database teardown defined in parent class """
        pass

NOTA: Se você excluir a conexão padrão da lista de conexões, não poderá usar os modelos do Django ou outros recursos que normalmente usam o banco de dados (obviamente não nos comunicamos com o banco de dados, mas o Django verifica os diferentes recursos que o DB suporta) . Também parece que o connections._connections não suporta __getitem__mais. Use connections._connections.defaultpara acessar o objeto.
8133 the_drow

2

Outra solução seria ter sua classe de teste simplesmente herdada, em unittest.TestCasevez de qualquer uma das classes de teste do Django. Os documentos do Django ( https://docs.djangoproject.com/en/2.0/topics/testing/overview/#writing-tests ) contêm o seguinte aviso sobre isso:

O uso do unittest.TestCase evita o custo de executar cada teste em uma transação e liberar o banco de dados, mas se seus testes interagirem com o banco de dados, o comportamento deles variará com base na ordem em que o executor os executa. Isso pode levar a testes de unidade que passam quando executados isoladamente, mas falham quando executados em um conjunto.

No entanto, se seu teste não usar o banco de dados, esse aviso não será necessário e você poderá colher os benefícios de não precisar executar cada caso de teste em uma transação.


Parece que isso ainda cria e destrói o banco de dados, a única diferença é que ele não executa o teste em uma transação e não libera o banco de dados.
Cam Rail

0

As soluções acima também são boas. Mas a solução a seguir também reduzirá o tempo de criação do banco de dados se houver mais número de migrações. Durante o teste de unidade, executar o syncdb em vez de executar todas as migrações para o sul será muito mais rápido.

SOUTH_TESTS_MIGRATE = False # Para desativar as migrações e usar o syncdb


0

Meu host permite apenas a criação e remoção de bancos de dados a partir de sua GUI da Web, então eu estava recebendo o erro "Ocorreu um erro ao criar o banco de dados de teste: permissão negada" ao tentar executar python manage.py test.

Eu esperava usar a opção --keepdb no django-admin.py, mas ela não parece mais ser suportada no Django 1.7.

O que acabei fazendo foi modificar o código do Django em ... / django / db / backends / creation.py, especificamente as funções _create_test_db e _destroy_test_db.

Pois _create_test_dbeu comentei a cursor.execute("CREATE DATABASE ...linha e a substitui por passpara que o trybloco não estivesse vazio.

Pois _destroy_test_dbacabei de comentar cursor.execute("DROP DATABASE- não precisei substituí-lo por nada, porque já havia outro comando no bloco ( time.sleep(1)).

Depois disso, meus testes foram bem executados - embora eu tenha configurado uma versão test_ do meu banco de dados regular separadamente.

Esta não é uma ótima solução, é claro, porque ela quebrará se o Django for atualizado, mas eu tinha uma cópia local do Django devido ao uso do virtualenv, pelo menos tenho controle sobre quando / se atualizo para uma versão mais recente.


0

Outra solução não mencionada: foi fácil para mim implementar, porque eu já tenho vários arquivos de configurações (para local / teste / produção) que herdam de base.py. Portanto, diferente de outras pessoas, não precisei sobrescrever DATABASES ['padrão'], pois DATABASES não está definido em base.py

O SimpleTestCase ainda tentou se conectar ao meu banco de dados de teste e executar migrações. Quando criei um arquivo config / settings / test.py que não definiu DATABASES para nada, meus testes de unidade foram executados sem ele. Isso me permitiu usar modelos que tinham chave estrangeira e campos de restrição exclusivos. (A pesquisa inversa de chave estrangeira, que requer uma pesquisa de banco de dados, falha.)

(Django 2.0.6)

Snippets de código PS

PROJECT_ROOT_DIR/config/settings/test.py:
from .base import *
#other test settings

#DATABASES = {
# 'default': {
#   'ENGINE': 'django.db.backends.sqlite3',
#   'NAME': 'PROJECT_ROOT_DIR/db.sqlite3',
# }
#}

cli, run from PROJECT_ROOT_DIR:
./manage.py test path.to.app.test --settings config.settings.test

path/to/app/test.py:
from django.test import SimpleTestCase
from .models import *
#^assume models.py imports User and defines Classified and UpgradePrice

class TestCaseWorkingTest(SimpleTestCase):
  def test_case_working(self):
    self.assertTrue(True)
  def test_models_ok(self):
    obj = UpgradePrice(title='test',price=1.00)
    self.assertEqual(obj.title,'test')
  def test_more_complex_model(self):
    user = User(username='testuser',email='hi@hey.com')
    self.assertEqual(user.username,'testuser')
  def test_foreign_key(self):
    user = User(username='testuser',email='hi@hey.com')
    ad = Classified(user=user,headline='headline',body='body')
    self.assertEqual(ad.user.username,'testuser')
  #fails with error:
  def test_reverse_foreign_key(self):
    user = User(username='testuser',email='hi@hey.com')
    ad = Classified(user=user,headline='headline',body='body')
    print(user.classified_set.first())
    self.assertTrue(True) #throws exception and never gets here

0

Ao usar o corredor de teste do nariz (django-nose), você pode fazer algo assim:

my_project/lib/nodb_test_runner.py:

from django_nose import NoseTestSuiteRunner


class NoDbTestRunner(NoseTestSuiteRunner):
    """
    A test runner to test without database creation/deletion
    Used for integration tests
    """
    def setup_databases(self, **kwargs):
        pass

    def teardown_databases(self, old_config, **kwargs):
        pass

No seu settings.pyvocê pode especificar o executor de teste lá, ou seja,

TEST_RUNNER = 'lib.nodb_test_runner.NoDbTestRunner' . # Was 'django_nose.NoseTestSuiteRunner'

OU

Eu queria que ele executasse apenas testes específicos, então eu o executo da seguinte forma:

python manage.py test integration_tests/integration_*  --noinput --testrunner=lib.nodb_test_runner.NoDbTestRunner
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.