pruebas unitarias de django sin un db

126

¿Existe la posibilidad de escribir django unittests sin configurar un db? Quiero probar la lógica de negocios que no requiere la configuración de db. Y aunque es rápido configurar un db, realmente no lo necesito en algunas situaciones.

paweloque
fuente
Me pregunto si eso realmente importa. La base de datos se mantiene en memoria + si no tiene ningún modelo, no se realiza nada con la base de datos. Entonces, si no lo necesita, no configure modelos.
Torsten Engelbrecht
3
Tengo modelos, pero para esas pruebas no son relevantes. Y el db no se guarda en la memoria, sino que se crea en mysql, sin embargo, específicamente para este propósito. No es que quiera esto ... Tal vez podría configurar django para usar una base de datos en memoria para realizar pruebas. Sabes como hacer esto?
Paweloque
Oh, lo siento. Las bases de datos en memoria son solo el caso cuando utiliza una base de datos SQLite. Excepto esto, no veo una manera de evitar crear la base de datos de prueba. No hay nada de esto en los documentos + Nunca sentí la necesidad de evitarlo.
Torsten Engelbrecht
3
La respuesta aceptada no me funcionó. En cambio, esto funcionó perfectamente: caktusgroup.com/blog/2013/10/02/skipping-test-db-creation
Hugo Pineda

Respuestas:

122

Puede subclasificar DjangoTestSuiteRunner y anular los métodos setup_databases y teardown_databases para pasar.

Cree un nuevo archivo de configuración y configure TEST_RUNNER en la nueva clase que acaba de crear. Luego, cuando esté ejecutando su prueba, especifique su nuevo archivo de configuración con el indicador --settings.

Aquí esta lo que hice:

Cree un corredor de traje de prueba personalizado similar 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

Crea una configuración personalizada:

from mysite.settings import *

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

Cuando esté ejecutando sus pruebas, ejecútelo de la siguiente manera con el indicador --settings establecido en su nuevo archivo de configuración:

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

ACTUALIZACIÓN: abril / 2018

Desde Django 1.8, el módulo se movió a .django.test.simple.DjangoTestSuiteRunner 'django.test.runner.DiscoverRunner'

Para obtener más información, consulte la sección de documentación oficial sobre corredores de prueba personalizados.

mohi666
fuente
2
Este error surge cuando tiene pruebas que necesitan transacciones de base de datos. Obviamente, si no tiene una base de datos, no podrá ejecutar esas pruebas. Debe ejecutar sus pruebas por separado. Si solo ejecuta su prueba usando python manage.py test --settings = new_settings.py, ejecutará un montón de otras pruebas desde otras aplicaciones que pueden requerir una base de datos.
mohi666
55
Tenga en cuenta que deberá extender SimpleTestCase en lugar de TestCase para sus clases de prueba. TestCase espera una base de datos.
Ben Roberts
9
Si no desea utilizar un nuevo archivo de configuración, puede especificar el nuevo TestRunner en la línea de comando con la --testrunneropción.
Bran Handley
26
¡¡Gran respuesta!! En django 1.8, de django.test.simple import DjangoTestSuiteRunner se ha cambiado de django.test.runner import DiscoverRunner ¡Espero que ayude a alguien!
Josh Brown el
2
En Django 1.8 y superior, se puede hacer una ligera corrección al código anterior. La declaración de importación se puede cambiar a: desde django.test.runner import DiscoverRunner El NoDbTestRunner ahora debe extender la clase DiscoverRunner.
Aditya Satyavada
77

Generalmente las pruebas en una aplicación se pueden clasificar en dos categorías

  1. Pruebas unitarias, estas prueban los fragmentos de código individuales en la insolación y no requieren ir a la base de datos
  2. Casos de prueba de integración que realmente van a la base de datos y prueban la lógica totalmente integrada.

Django admite pruebas unitarias y de integración.

Las pruebas unitarias no requieren la configuración y el desmantelamiento de la base de datos, que debemos heredar de SimpleTestCase .

from django.test import SimpleTestCase


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

Para los casos de prueba de integración, heredar de TestCase a su vez hereda de TransactionTestCase y configurará y destruirá la base de datos antes de ejecutar cada prueba.

from django.test import TestCase


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

Esta estrategia asegurará que la base de datos se cree y destruya solo para los casos de prueba que acceden a la base de datos y, por lo tanto, las pruebas serán más eficientes

Ali
fuente
37
Esto podría hacer que las pruebas en ejecución sean más eficientes, pero tenga en cuenta que el corredor de pruebas aún crea bases de datos de prueba en la inicialización.
Monkut
66
Tanto más simple que la respuesta elegida. Muchas gracias!
KFunk
1
@monkut No ... si solo tiene la clase SimpleTestCase, el corredor de prueba no ejecuta nada, vea este proyecto .
Claudio Santos
Django seguirá intentando crear una base de datos de prueba incluso si solo usa SimpleTestCase. Ver esta pregunta .
Marko Prcać
el uso de SimpleTestCase funciona exactamente para probar métodos de utilidad o fragmentos y no usa ni crea db de prueba. ¡Exactamente lo que necesito!
Tyro Hunter
28

De django.test.simple

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

Entonces anular en DiscoverRunnerlugar 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

Usar así:

python manage.py test app --testrunner=app.filename.NoDbTestRunner
themadmax
fuente
8

Elegí heredar django.test.runner.DiscoverRunnery hacer un par de adiciones al run_testsmétodo.

Mi primera adición verifica si es necesario configurar una base de datos y permite que la setup_databasesfuncionalidad normal se active si es necesaria una base de datos. Mi segunda adición permite teardown_databasesque se ejecute lo normal si setup_databasesse permitió la ejecución del método.

Mi código asume que cualquier TestCase que hereda de django.test.TransactionTestCase(y por lo tanto django.test.TestCase) requiere que se configure una base de datos. Hice esta suposición porque los documentos de Django dicen:

Si necesita cualquiera de las otras características específicas de Django más complejas y pesadas como ... Probar o usar el ORM ... entonces debería usar TransactionTestCase o TestCase en su lugar.

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)

Finalmente, agregué la siguiente línea al archivo settings.py de mi proyecto.

mysite / settings.py

TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner'

Ahora, cuando ejecuto solo pruebas no dependientes de db, ¡mi conjunto de pruebas ejecuta un orden de magnitud más rápido! :)

Pablo
fuente
6

Actualizado: también vea esta respuesta para usar una herramienta de terceros pytest.


@Cesar tiene razón. Después de ejecutarse accidentalmente ./manage.py test --settings=no_db_settings, sin especificar el nombre de una aplicación, mi base de datos de desarrollo se borró.

Para una forma más segura, use lo mismo NoDbTestRunner, pero junto con lo siguiente 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'

Necesita crear una base de datos llamada _test_mysite_dbusando una herramienta de base de datos externa. Luego ejecute el siguiente comando para crear las tablas correspondientes:

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

Si está utilizando South, también ejecute el siguiente comando:

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

¡OKAY!

Ahora puede ejecutar pruebas unitarias increíblemente rápido (y seguro):

./manage.py test myapp --settings=mysite.no_db_settings
Rockallita
fuente
He realizado pruebas usando pytest (con el complemento pytest-django) y NoDbTestRunner, si de alguna manera crea un objeto por accidente en un caso de prueba y no anula el nombre de la base de datos, el objeto se creará en sus bases de datos locales que configure en el ajustes El nombre 'NoDbTestRunner' debería ser 'NoTestDbTestRunner' porque no creará la base de datos de prueba, pero usará su base de datos desde la configuración.
Gabriel Muj
2

Como alternativa a la modificación de su configuración para que NoDbTestRunner sea "seguro", aquí hay una versión modificada de NoDbTestRunner que cierra la conexión de la base de datos actual y elimina la información de conexión de la configuración y el objeto de conexión. Funciona para mí, pruébalo en tu entorno antes de confiar en él :)

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
Tecuya
fuente
NOTA: Si elimina la conexión predeterminada de la lista de conexiones, no podrá usar los modelos de Django u otras características que normalmente usan la base de datos (obviamente no nos comunicamos con la base de datos, pero Django verifica las diferentes características que admite la base de datos) . También parece que connections._connections ya no es compatible __getitem__. Úselo connections._connections.defaultpara acceder al objeto.
the_drow
2

Otra solución sería tener su clase de prueba simplemente heredando en unittest.TestCaselugar de cualquiera de las clases de prueba de Django. Los documentos de Django ( https://docs.djangoproject.com/en/2.0/topics/testing/overview/#writing-tests ) contienen la siguiente advertencia al respecto:

El uso de unittest.TestCase evita el costo de ejecutar cada prueba en una transacción y vaciar la base de datos, pero si sus pruebas interactúan con la base de datos, su comportamiento variará en función del orden en que el corredor de pruebas las ejecute. Esto puede conducir a pruebas unitarias que pasan cuando se ejecutan de forma aislada pero fallan cuando se ejecutan en un conjunto.

Sin embargo, si su prueba no utiliza la base de datos, esta advertencia no tiene por qué preocuparle y puede obtener los beneficios de no tener que ejecutar cada caso de prueba en una transacción.

Kurt Peek
fuente
Parece que esto todavía crea y destruye la base de datos, la única diferencia es que no ejecuta la prueba en una transacción y no descarga la base de datos.
Cam Rail
0

Las soluciones anteriores también están bien. Pero la siguiente solución también reducirá el tiempo de creación de db si hay más migraciones. Durante las pruebas unitarias, ejecutar syncdb en lugar de ejecutar todas las migraciones hacia el sur será mucho más rápido.

SOUTH_TESTS_MIGRATE = False # Para deshabilitar las migraciones y usar syncdb en su lugar

Venkat
fuente
0

Mi servidor web solo permite crear y descartar bases de datos desde su GUI web, por lo que recibí un error "Tengo un error al crear la base de datos de prueba: Permiso denegado" al intentar ejecutar python manage.py test.

Esperaba usar la opción --keepdb para django-admin.py, pero parece que ya no se admite a partir de Django 1.7.

Lo que terminé haciendo fue modificar el código de Django en ... / django / db / backends / creation.py, específicamente las funciones _create_test_db y _destroy_test_db.

Porque _create_test_dbcomenté la cursor.execute("CREATE DATABASE ...línea y la reemplacé passpara que el trybloque no estuviera vacío.

Para _destroy_test_dbacabo comentada cursor.execute("DROP DATABASE- Yo no tenía necesidad de reemplazarlo con nada porque ya había otro comando en el bloque ( time.sleep(1)).

Después de eso, mis pruebas funcionaron bien, aunque configuré una versión test_ de mi base de datos regular por separado.

Por supuesto, esta no es una gran solución, porque se romperá si se actualiza Django, pero tenía una copia local de Django debido al uso de virtualenv, así que al menos tengo control sobre cuándo / si me actualizo a una versión más nueva.

Chirael
fuente
0

Otra solución no mencionada: esto fue fácil de implementar porque ya tengo múltiples archivos de configuración (para local / puesta en escena / producción) que heredan de base.py. Así que, a diferencia de otras personas, no tuve que sobrescribir BASES DE DATOS ['predeterminado'], ya que BASES DE DATOS no está configurado en base.py

SimpleTestCase todavía intentó conectarse a mi base de datos de prueba y ejecutar migraciones. Cuando hice un archivo config / settings / test.py que no configuró BASES DE DATOS para nada, entonces las pruebas de mi unidad se ejecutaron sin él. Me permitió usar modelos que tenían clave externa y campos de restricción únicos. (La búsqueda de clave externa inversa, que requiere una búsqueda de db, falla).

(Django 2.0.6)

Fragmentos 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='[email protected]')
    self.assertEqual(user.username,'testuser')
  def test_foreign_key(self):
    user = User(username='testuser',email='[email protected]')
    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='[email protected]')
    ad = Classified(user=user,headline='headline',body='body')
    print(user.classified_set.first())
    self.assertTrue(True) #throws exception and never gets here
Simone
fuente
0

Al usar el corredor de prueba de nariz (django-nose), puede hacer algo como esto:

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

En su settings.pypuede especificar el corredor de prueba allí, es decir

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

O

Lo quería solo para ejecutar pruebas específicas, así que lo ejecuto así:

python manage.py test integration_tests/integration_*  --noinput --testrunner=lib.nodb_test_runner.NoDbTestRunner
radtek
fuente