没有数据库的 django 单元测试

是否有可能在不设置 db 的情况下编写 django 单元测试?我想测试不需要设置数据库的业务逻辑。虽然设置一个 db 很快,但是在某些情况下我真的不需要它。

49962 次浏览

您可以子类化 DjangoTestSuiteRunner 并覆盖 setup _ database 和 teardown _ database 方法来传递。

创建一个新的设置文件,并将 TEST _ RUNNER 设置为刚才创建的新类。然后在运行测试时,使用—— sets 标志指定新的设置文件。

我是这么做的:

创建一个类似下面这样的自定义测试套装运动员:

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'

当您运行测试时,像下面这样运行——将设置标志设置为新的设置文件:

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

更新: 2018年4月

从 Django 1.8,模块 django.test.simple.DjangoTestSuiteRunner 被感动了'django.test.runner.DiscoverRunner'

有关更多信息,请查看关于自定义测试运行程序的 官方文件部分。

更新: 也请参阅使用第三方工具 pytest这个答案


@ Cesar 是对的,在不小心运行了 ./manage.py test --settings=no_db_settings之后,没有指定应用程序名,我的开发数据库被清空了。

为了更安全,使用相同的 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

如果您正在使用 South,请运行以下命令:

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

好的!

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

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

作为修改设置以使 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

一般来说,应用程序中的测试可以分为两类

  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)

这个策略将确保数据库在创建和销毁时仅用于访问数据库的测试用例,因此测试将更有效率

来自 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 --testrunner=app.filename.NoDbTestRunner app

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

禁用迁移并使用 syncdb 取而代之

我选择从 django.test.runner.DiscoverRunner继承,并对 run_tests方法做了一些补充。

我的第一个附加检查是否设置数据库是必要的,并允许正常的 setup_databases功能启动,如果数据库是必要的。如果允许 setup_databases方法运行,我的第二个附加允许正常的 teardown_databases运行。

我的代码假设从 django.test.TransactionTestCase(因此是 django.test.TestCase)继承的任何 TestCase 都需要设置一个数据库。我做出这个假设是因为姜戈的医生说:

如果您需要任何其他更复杂和重量级的 Django 特定特性,比如... 测试或使用 ORM... 那么您应该改用 TransactionTestCase 或 TestCase。

Https://docs.djangoproject.com/en/1.6/topics/testing/tools/#django.test 简单测试案例

Mysite/script/setings.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)

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

Mysite/setings.py

TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner'

现在,当只运行非 db 依赖的测试时,我的测试套件运行数量级更快! :)

我的 Web 主机只允许从他们的 Web GUI 创建和删除数据库,所以我在尝试运行 python manage.py test时得到一个“创建测试数据库时出错: 拒绝许可”错误。

我希望在 Django-admin.py 中使用—— keepdb 选项,但是在 Django 1.7中似乎不再支持它了。

最后,我修改了.../Django/db/backend/creation.py 中的 Django 代码,特别是 _ create _ test _ db 和 _ broke _ test _ db 函数。

对于 _create_test_db,我注释掉了 cursor.execute("CREATE DATABASE ...行,并将其替换为 pass,这样 try块就不会是空的。

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

在那之后,我的测试运行得很好——尽管我分别设置了常规数据库的 test _ version。

当然,这不是一个很好的解决方案,因为如果 Django 升级了,它就会崩溃,但是由于使用 viralenv,我有一个 Django 的本地副本,所以至少我可以控制何时/如果我升级到一个新版本。

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

使用单元测试。TestCase 避免了在事务中运行每个测试和刷新数据库的成本,但是如果您的测试与数据库交互,它们的行为将根据测试运行程序执行它们的顺序而变化。这可能导致单元测试在单独运行时通过,但在套件中运行时失败。

但是,如果您的测试没有使用数据库,那么这个警告不需要关心您,您可以获得不必在事务中运行每个测试用例的好处。

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

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

(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

当使用鼻子测试运行程序(django-nose)时,您可以执行以下操作:

返回文章页面

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

您可以从 django.test 将数据库设置为普通 TestCase 中的空列表。

from django.test import TestCase


class NoDbTestCase(TestCase):
databases = []