没有数据库的Django单元测试


126

是否可以在不设置数据库的情况下编写Django单元测试?我想测试不需要设置数据库的业务逻辑。尽管设置数据库的速度很快,但在某些情况下我真的不需要它。


我想知道这是否真的重要。数据库保留在内存中+如果您没有任何模型,则数据库将不执行任何操作。因此,如果您不需要它,则无需设置模型。
Torsten Engelbrecht

3
我确实有模型,但是对于那些测试,它们并不相关。而且,db并不保留在内存中,而是专门在mysql中建立的。不是我想要这个。也许我可以配置django以使用内存中的数据库进行测试。你知道怎么做吗?
paweloque 2011年

哦,对不起。使用SQLite数据库时,内存数据库就是这种情况。除此之外,我没有看到避免创建测试数据库的方法。在文档中没有关于此的内容+我从没有感到需要避免它。
Torsten Engelbrecht,

3
接受的答案对我没有帮助。取而代之的是,这非常有效:caktusgroup.com/blog/2013/10/02/skipping-test-db-creation
Hugo Pineda

Answers:


122

您可以继承DjangoTestSuiteRunner的子类,并覆盖setup_databases和teardown_databases方法以传递。

创建一个新的设置文件,并将TEST_RUNNER设置为刚创建的新类。然后,在运行测试时,请使用--settings标志指定新的设置文件。

这是我所做的:

创建一个类似于以下内容的自定义测试服跑步者:

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

创建自定义设置:

from mysite.settings import *

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

在运行测试时,将--settings标志设置为新的设置文件,如下所示运行它:

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

更新:2018年4月

由于Django的1.8,该模块被移动到。django.test.simple.DjangoTestSuiteRunner 'django.test.runner.DiscoverRunner'

有关更多信息,请参阅有关自定义测试运行程序的官方文档部分。


2
当您有需要数据库事务的测试时,会出现此错误。显然,如果您没有数据库,则将无法运行这些测试。您应该单独运行测试。如果您只是使用python manage.py test --settings = new_settings.py来运行测试,它将从可能需要数据库的其他应用程序中运行大量其他测试。
mohi666

5
请注意,您需要为测试类扩展SimpleTestCase而不是TestCase。TestCase需要一个数据库。
本·罗伯茨

9
如果您不想使用新的设置文件,则可以在命令行中使用--testrunner选项指定新的TestRunner 。
布兰·汉德利

26
好答案!在django 1.8中,从django.test.simple导入DjangoTestSuiteRunner已更改为从django.test.runner导入DiscoverRunner希望对您有所帮助!
乔什·布朗

2
在Django 1.8及更高版本中,可以对上述代码进行一些更正。导入语句可以更改为:从django.test.runner import DiscoverRunner NoDbTestRunner现在必须扩展DiscoverRunner类。
Aditya Satyavada

77

通常,应用程序中的测试可以分为两类

  1. 单元测试,这些测试单独测试各个代码段,并且不需要进入数据库
  2. 集成测试用例实际上是进入数据库并测试完全集成的逻辑的。

Django支持单元测试和集成测试。

单元测试不需要设置和拆除数据库,而这些我们应该从SimpleTestCase继承。

from django.test import SimpleTestCase


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

对于集成测试用例,继承自TestCase的内容又继承自TransactionTestCase,它将在运行每个测试之前设置和拆除数据库。

from django.test import TestCase


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

该策略将确保仅针对访问数据库的测试用例创建和销毁数据库,因此测试将更加高效


37
这可能使运行测试更加有效,但是请注意,测试运行器仍在初始化时创建测试数据库。
monkut

6
如此简单,以至于选择的答案。非常感谢!
KFunk '16

1
@monkut否...如果您只有SimpleTestCase类,那么测试运行程序什么也不运行,请参见此项目
克劳迪奥·桑托斯

即使您仅使用SimpleTestCase,Django仍将尝试创建测试数据库。看到这个问题
MarkoPrcać19年

使用SimpleTestCase完全适用于测试实用程序方法或代码片段,并且不使用或创建测试数据库。正是我需要的!
泰罗·亨特

28

django.test.simple

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

因此,请覆盖DiscoverRunner而不是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

这样使用:

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

8

我选择继承自django.test.runner.DiscoverRunnerrun_tests方法,并对其进行了一些补充。

我的第一个添加项检查是否需要设置数据库,并在需要数据库的情况setup_databases下启动正常功能。teardown_databases如果setup_databases允许该方法运行,我的第二次添加使法线运行。

我的代码假定任何继承自django.test.TransactionTestCase(因此django.test.TestCase)的TestCase都需要设置数据库。我做这个假设是因为Django文档说:

如果您需要其他任何更复杂且重量级的Django特定功能,例如...测试或使用ORM ...,则应改用TransactionTestCase或TestCase。

https://docs.djangoproject.com/zh-CN/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)

最后,我将以下行添加到项目的settings.py文件中。

mysite / settings.py

TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner'

现在,当仅运行与数据库无关的测试时,我的测试套件的运行速度提高了一个数量级!:)


6

更新:也请参见使用第三方工具的答案pytest


@Cesar是正确的。意外运行后./manage.py test --settings=no_db_settings,没有指定应用程序名称,我的开发数据库被清除了。

为了安全起见,请使用same NoDbTestRunner,但要结合以下使用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'

您需要_test_mysite_db使用外部数据库工具创建一个名为的数据库。然后运行以下命令来创建相应的表:

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

如果您使用的是南方,还请运行以下命令:

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

好!

现在,您可以通过以下方法快速(安全)地运行单元测试:

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

我已经使用pytest(带有pytest-django插件)和NoDbTestRunner进行了测试,如果您以某种方式在测试用例中偶然创建了一个对象,并且您没有覆盖数据库名称,那么该对象将在您在设置。名称“ NoDbTestRunner”应为“ NoTestDbTestRunner”,因为它不会创建测试数据库,但会使用设置中的数据库。
加布里埃尔·穆伊

2

作为修改设置以使NoDbTestRunner“安全”的一种替代方法,这是NoDbTestRunner的修改版本,它关闭当前数据库连接并从设置和连接对象中删除连接信息。为我工作,在依赖它之前先在您的环境中对其进行测试:)

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

注意:如果从连接列表中删除默认连接,则将无法使用Django模型或通常使用数据库的其他功能(显然,我们不与数据库进行通信,但Django检查数据库支持的其他功能) 。同样,connections._connections似乎不再受支持__getitem__。使用connections._connections.default为了访问对象。
the_drow 2013年

2

另一个解决方案是让您的测试类简单地继承unittest.TestCase而不是Django的任何测试类。Django文档(https://docs.djangoproject.com/zh-CN/2.0/topics/testing/overview/#writing-tests)包含以下有关此的警告:

使用unittest.TestCase可以避免在事务中运行每个测试并刷新数据库的开销,但是如果您的测试与数据库进行交互,则其行为将根据测试运行者执行它们的顺序而有所不同。这可能导致单元测试在单独运行时通过,但在套件中运行时失败。

但是,如果您的测试未使用数据库,则此警告与您无关,您可以从不必在事务中运行每个测试用例的好处中获益。


看起来这仍然会创建和销毁数据库,唯一的区别是,它不会在事务中运行测试,也不会刷新数据库。
Cam Rail

0

上述解决方案也很好。但是,如果迁移数量更多,以下解决方案还将减少数据库创建时间。在单元测试期间,运行syncdb而不是运行所有向南的迁移会更快。

SOUTH_TESTS_MIGRATE = False#要禁用迁移并改用syncdb


0

我的Web主机仅允许从其Web GUI创建和删除数据库,因此在尝试运行时遇到了“创建测试数据库时出错:权限被拒绝”错误python manage.py test

我希望对django-admin.py使用--keepdb选项,但是从Django 1.7开始似乎不再受支持。

我最终要做的是修改... / django / db / backends / creation.py中的Django代码,特别是_create_test_db和_destroy_test_db函数。

因为_create_test_db我注释掉了这一cursor.execute("CREATE DATABASE ...行并将其替换为该行,pass所以该try块不会为空。

因为_destroy_test_db我刚刚注释掉了cursor.execute("DROP DATABASE-我不需要用任何东西替换它,因为代码块(time.sleep(1))中已经有另一个命令。

之后,我的测试运行良好-尽管我确实单独设置了常规数据库的test_版本。

当然,这不是一个很好的解决方案,因为如果升级Django,它会中断,但是由于使用virtualenv,我拥有Django的本地副本,因此至少我可以控制何时/是否升级到较新的版本。


0

另一个未提及的解决方案:这对我来说很容易实现,因为我已经有多个设置文件(用于本地/暂存/生产),这些文件是从base.py继承的。因此,与其他人不同,我无需覆盖DATABASES ['default'],因为未在base.py中设置DATABASES

SimpleTestCase仍尝试连接到我的测试数据库并运行迁移。当我创建一个config / settings / test.py文件时,它没有将DATABASES设置为任何东西,然后我的单元测试就没有了。它允许我使用具有外键和唯一约束字段的模型。(需要db查找的反向外键查找失败。)

(Django 2.0.6)

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

使用鼻子测试跑步机(django-nose)时,您可以执行以下操作:

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

在你的settings.py,你可以在那里指定的测试运行,即

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

要么

我只希望它用于运行特定的测试,所以我可以这样运行:

python manage.py test integration_tests/integration_*  --noinput --testrunner=lib.nodb_test_runner.NoDbTestRunner
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.