测试工具

Django提供了一小组工具,在编写测试时可以派上用场。

测试客户端

测试客户端是一个Python类,充当虚拟Web浏览器,允许您测试视图并以编程方式与Django驱动的应用程序进行交互。

测试客户端可以做的一些事情是:

  • 在URL上模拟GET和POST请求并观察响应 - 从低级HTTP(结果标题和状态码)到页面内容的所有内容。
  • 查看重定向链(如果有的话),并检查每一步的URL和状态码。
  • 测试一个给定的请求是由一个给定的Django模板呈现的,其中包含特定值的模板上下文。

请注意,测试客户端并不是要替代Selenium或其他“浏览器内”框架。 Django的测试客户端有不同的焦点。 简而言之:

  • 使用Django的测试客户端来确定正在呈现正确的模板,并且模板被传递正确的上下文数据。
  • 使用浏览器内部框架(如Selenium)来测试呈现的 HTML和网页的行为,即JavaScript功能。 Django也为这些框架提供了特别的支持。有关更多详细信息,请参阅LiveServerTestCase部分。

综合测试套件应该使用两种测试类型的组合。

概述和一个简单的例子

要使用测试客户端,请实例化django.test.Client并检索网页:

>>> from django.test import Client
>>> c = Client()
>>> response = c.post('/login/', {'username': 'john', 'password': 'smith'})
>>> response.status_code
200
>>> response = c.get('/customer/details/')
>>> response.content
b'<!DOCTYPE html...'

如本例所示,您可以在Python交互式解释器的会话中实例化Client

请注意关于测试客户端如何工作的一些重要事情:

  • The test client does not require the Web server to be running. 事实上,它将运行得很好,没有任何Web服务器运行! 这是因为它避免了HTTP的开销并直接处理Django框架。 这有助于使单元测试快速运行。

  • 在检索页面时,请记住指定URL的路径,而不是整个域。 例如,这是正确的:

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

    这是不正确的:

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

    测试客户端无法检索到不支持Django项目的网页。 如果您需要检索其他网页,请使用Python标准库模块(如urllib)。

  • To resolve URLs, the test client uses whatever URLconf is pointed-to by your ROOT_URLCONF setting.

  • 虽然上面的例子可以在Python交互式解释器中使用,但是一些测试客户端的功能,特别是与模板相关的功能,只有在测试运行的时候才可用。

    其原因是Django的测试运行者执行一些黑魔法来确定哪个模板是由给定视图加载的。 这个黑魔法(本质上是在内存中修补了Django的模板系统)只在测试运行时发生。

  • 默认情况下,测试客户端将禁用您的站点执行的任何CSRF检查。

    If, for some reason, you want the test client to perform CSRF checks, you can create an instance of the test client that enforces CSRF checks. 为此,在构建客户端时传入enforce_csrf_checks参数:

    >>> from django.test import Client
    >>> csrf_client = Client(enforce_csrf_checks=True)
    

发出请求

使用django.test.Client类发出请求。

Clientenforce_csrf_checks = False** defaults[source]

施工时不需要任何争论。 但是,您可以使用关键字参数来指定一些默认标题。 例如,这将在每个请求中发送一个User-Agent HTTP标头:

>>> c = Client(HTTP_USER_AGENT='Mozilla/5.0')

传递给get()post()等的extra关键字参数的值优先于传递给类构造函数的默认值。

可以使用enforce_csrf_checks来测试CSRF保护(见上)。

一旦你有一个Client实例,你可以调用以下任何一种方法:

getpathdata = Nonefollow = Falsesecure = False** extra[source]

在提供的path上发出GET请求,并返回一个Response对象,如下所述。

The key-value pairs in the data dictionary are used to create a GET data payload. 例如:

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

...将导致对GET请求的评估等同于:

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

可以使用extra关键字参数参数指定要在请求中发送的标头。 例如:

>>> 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.

CGI规范

通过**extra发送的头文件应遵循CGI规范。 例如,模拟HTTP请求中从浏览器发送到服务器的不同的“主机”头文件应该作为HTTP_HOST传递。

如果您已经以URL编码形式获得GET参数,则可以使用该编码,而不是使用data参数。 例如,以前的GET请求也可以作为:

>>> c = Client()
>>> c.get('/customers/details/?name=fred&age=7')

如果您提供的URL包含编码的GET数据和数据参数,则数据参数将优先。

If you set follow to True the client will follow any redirects and a redirect_chain attribute will be set in the response object containing tuples of the intermediate urls and status codes.

如果你有一个重定向到/next/的URL /redirect_me/,重定向到/final/,这就是你会看到的:

>>> response = c.get('/redirect_me/', follow=True)
>>> response.redirect_chain
[('http://testserver/next/', 302), ('http://testserver/final/', 302)]

如果将secure设置为True,则客户端将模拟HTTPS请求。

postpathdata = Nonecontent_type = MULTIPART_CONTENTfollow = Falsesecure = False** extra[source]

在提供的path上发出POST请求,并返回一个Response对象,如下所述。

data字典中的键值对用于提交POST数据。 例如:

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

...将导致对此URL的POST请求进行评估:

/login/

...这个POST数据:

name=fred&passwd=secret

如果您提供了content_type(例如text / xml用于XML负载),则data的内容将在POST请求中按原样发送,在HTTP Content-Type标题中使用content_type

If you don’t provide a value for content_type, the values in data will be transmitted with a content type of multipart/form-data. 在这种情况下,data中的键值对将被编码为多部分消息,并用于创建POST数据有效负载。

要为给定键提交多个值 - 例如,要指定&lt; select 多个&gt;的选择,请将值作为列表或元组所需的密钥。 例如,这个data的值将为名为choices的字段提交三个选定的值:

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

提交文件是一个特例。 要发布文件,只需要提供文件字段名称作为密钥,并将文件句柄提供给要上载的文件作为值。 例如:

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

(名字attachment在这里是不相关的;使用任何你的文件处理代码的名字。)

您也可以提供任何类似文件的对象(例如,StringIOBytesIO)作为文件句柄。 If you’re uploading to an ImageField, the object needs a name attribute that passes the validate_image_file_extension validator. 例如:

>>> from io import BytesIO
>>> img = BytesIO(b'mybinarydata')
>>> img.name = 'myimage.jpg'

请注意,如果您希望对多个post()调用使用相同的文件句柄,则需要手动重置帖子之间的文件指针。 最简单的方法是在文件提供给post()之后手动关闭文件,如上所示。

您还应该确保文件以允许读取数据的方式打开。 如果您的文件包含二进制数据(如图像),则意味着您需要以rb(读二进制)模式打开文件。

extra参数的作用与Client.get()相同。

如果您通过POST请求的URL包含编码参数,则这些参数将在request.GET数据中可用。 例如,如果您要提出请求:

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

...处理这个请求的视图可以询问request.POST来检索用户名和密码,并且可以询问request.GET来确定用户是否是访问者。

If you set follow to True the client will follow any redirects and a redirect_chain attribute will be set in the response object containing tuples of the intermediate urls and status codes.

如果将secure设置为True,则客户端将模拟HTTPS请求。

headpathdata = Nonefollow = Falsesecure = False** extra[source]

在提供的path上进行HEAD请求并返回Response对象。 这个方法就像Client.get()一样工作,包括followsecureextra不返回消息正文。

options(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)[source]

在提供的path上做出OPTIONS请求并返回Response对象。 用于测试RESTful接口。

当提供data时,它被用作请求体,Content-Type标题被设置为content_type

followsecureextra自变量与Client.get()相同。

put(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)[source]

在提供的path上发出PUT请求,并返回Response对象。 用于测试RESTful接口。

当提供data时,它被用作请求体,Content-Type标题被设置为content_type

followsecureextra自变量与Client.get()相同。

patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)[source]

在提供的path上发出PATCH请求,并返回Response对象。 用于测试RESTful接口。

followsecureextra自变量与Client.get()相同。

delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)[source]

在提供的path上执行DELETE请求并返回Response对象。 用于测试RESTful接口。

当提供data时,它被用作请求体,Content-Type标题被设置为content_type

followsecureextra自变量与Client.get()相同。

trace(path, follow=False, secure=False, **extra)[source]

在提供的path上发出TRACE请求并返回Response对象。 用于模拟诊断探针。

与其他请求方法不同,为了符合 RFC 7231#section-4.3.8,没有提供data作为关键字参数,规定TRACE请求不得有身体。

followsecureextra自变量的作用与Client.get()相同。

login(**credentials)[source]

如果您的站点使用Django的authentication system并且处理登录用户,则可以使用测试客户端的login()方法模拟用户登录到站点的效果。

在你调用这个方法之后,测试客户端将拥有通过任何可能构成视图一部分的基于登录测试的所有cookies和会话数据。

credentials参数的格式取决于您正在使用哪个authentication backend(由您的AUTHENTICATION_BACKENDS设置进行配置)。 如果您使用的是由Django(ModelBackend)提供的标准身份验证后端,那么credentials应该是用户的用户名和密码,作为关键字参数提供:

>>> c = Client()
>>> c.login(username='fred', password='secret')

# Now you can access a view that's only available to logged-in users.

如果您使用不同的身份验证后端,则此方法可能需要不同的凭据。 它需要你的后端的authenticate()方法需要任何证书。

login() returns True if it the credentials were accepted and login was successful.

最后,您需要记住创建用户帐户,然后才能使用此方法。 正如我们上面所解释的那样,测试运行器是使用测试数据库执行的,默认情况下不包含用户。 因此,您的生产站点上有效的用户帐户在测试条件下将不起作用。 您将需要创建用户作为测试套件的一部分 - 手动(使用Django模型API)或测试夹具。 请记住,如果您希望测试用户拥有密码,则不能通过直接设置密码属性来设置用户密码 - 您必须使用set_password()函数存储正确哈希密码。 或者,您可以使用create_user()辅助方法使用正确哈希密码创建新用户。

force_loginuserbackend = None[source]

如果您的站点使用Django的authentication system,则可以使用force_login()方法来模拟用户登录到站点的效果。 当测试需要用户登录时,使用此方法而不是login(),用户登录的详细信息并不重要。

login()不同,此方法会跳过身份验证和验证步骤:允许非活动用户(is_active=False)登录,并且不需要提供用户凭证。

用户将backend属性设置为backend参数(应该是一个点缀的Python路径字符串)或settings.AUTHENTICATION_BACKENDS[0]如果没有提供一个值。 login()调用的authenticate()函数通常以这种方式注释用户。

这个方法比login()更快,因为绕过了昂贵的密码散列算法。 Also, you can speed up login() by using a weaker hasher while testing.

logout()[source]

如果您的站点使用Django的authentication system,则可以使用logout()方法模拟用户注销站点的效果。

在你调用这个方法之后,测试客户端将把所有的cookies和会话数据清除为默认值。 后续请求将显示来自AnonymousUser

测试响应

get()post()方法都返回一个Response对象。 这个Response对象与不同与Django视图返回的HttpResponse对象相同。测试响应对象有一些额外的数据可用于验证测试代码。

具体而言,Response对象具有以下属性:

响应 T0> ¶ T1>
客户端 T0> ¶ T1>

用于完成请求的测试客户端导致了响应。

含量 T0> ¶ T1>

响应的主体,作为一个字节串。 这是视图呈现的最终页面内容,或任何错误消息。

上下文 T0> ¶ T1>

用于呈现生成响应内容的模板的模板Context实例。

如果呈现的页面使用多个模板,那么context将按照呈现顺序成为Context对象的列表。

无论渲染过程中使用的模板数量如何,您都可以使用[]运算符来检索上下文值。 例如,上下文变量name可以使用以下方式获取:

>>> response = client.get('/foo/')
>>> response.context['name']
'Arthur'

不使用Django模板?

该属性仅在使用DjangoTemplates后端时填充。 如果您使用的是另一个模板引擎,那么context_data可能是使用该属性的响应的合适替代方案。

JSON T0>( ** kwargs T1>)¶ T2>

响应的正文,解析为JSON。 额外的关键字参数传递给json.loads() 例如:

>>> response = client.get('/foo/')
>>> response.json()['name']
'Arthur'

如果Content-Type头部不是"application/json",那么当尝试解析响应时会引发ValueError

请求 T0> ¶ T1>

刺激响应的请求数据。

wsgi_request T0> ¶ T1>

生成响应的测试处理程序生成的WSGIRequest实例。

STATUS_CODE T0> ¶ T1>

响应的HTTP状态,作为整数。 有关已定义代码的完整列表,请参阅IANA状态代码注册表

模板 T0> ¶ T1>

用于呈现最终内容的Template实例列表,按照呈现顺序显示。 对于列表中的每个模板,如果模板是从文件加载的,则使用template.name获取模板的文件名。 (该名称是一个字符串,如'admin/index.html'。)

不使用Django模板?

该属性仅在使用DjangoTemplates后端时填充。 如果您使用的是另一个模板引擎,那么如果您只需要用于渲染的模板的名称,template_name可能是一个合适的选择。

resolver_match T0> ¶ T1>

响应的ResolverMatch的一个实例。 例如,可以使用func属性来验证提供响应的视图:

# my_view here is a function based view
self.assertEqual(response.resolver_match.func, my_view)

# class-based views need to be compared by name, as the functions
# generated by as_view() won't be equal
self.assertEqual(response.resolver_match.func.__name__, MyView.as_view().__name__)

如果找不到给定的URL,则访问该属性将引发一个Resolver404异常。

您还可以使用响应对象上的字典语法来查询HTTP标头中任何设置的值。 例如,您可以使用response['Content-Type']确定响应的内容类型。

例外¶ T0>

如果您将测试客户端指向引发异常的视图,那么该异常将在测试用例中可见。 您可以使用标准 尝试 ... 块或assertRaises()来测试异常。

测试客户端不可见的唯一例外是Http404PermissionDeniedSystemExitSuspiciousOperation Django在内部捕获这些异常并将它们转换为适当的HTTP响应代码。 在这些情况下,你可以在你的测试中检查response.status_code

持久状态

测试客户端是有状态的。 如果一个响应返回一个cookie,那么这个cookie将被存储在测试客户端中,并与所有后续的get()post()请求一起发送。

没有遵循这些cookie的过期政策。 如果你想要一个cookie过期,可以手动删除它或创建一个新的Client实例(这将有效地删除所有的cookie)。

测试客户端有两个存储持久状态信息的属性。 您可以作为测试条件的一部分访问这些属性。

客户。饼干 T0> ¶ T1>

一个Python SimpleCookie对象,包含所有客户端cookie的当前值。 有关更多信息,请参阅http.cookies模块的文档。

客户。会话 T0> ¶ T1>

包含会话信息的类似字典的对象。 有关完整的详细信息,请参阅session documentation

要修改会话然后保存它,它必须首先存储在一个变量中(因为每次访问这个属性时都会创建一个新的SessionStore):

def test_something(self):
    session = self.client.session
    session['somekey'] = 'test'
    session.save()

设置语言

在测试支持国际化和本地化的应用程序时,您可能需要为测试客户端请求设置语言。 这样做的方法取决于是否启用了LocaleMiddleware

如果启用了中间件,则可以通过创建一个名为LANGUAGE_COOKIE_NAME的cookie和一个语言代码值来设置该语言:

from django.conf import settings

def test_language_using_cookie(self):
    self.client.cookies.load({settings.LANGUAGE_COOKIE_NAME: 'fr'})
    response = self.client.get('/')
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

或者在请求中包含Accept-Language HTTP标头:

def test_language_using_header(self):
    response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='fr')
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

更多细节在How Django discovers language preference

如果中间件未启用,则可以使用translation.override()来设置活动语言:

from django.utils import translation

def test_language_using_override(self):
    with translation.override('fr'):
        response = self.client.get('/')
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

更多细节在Explicitly setting the active language

实施例¶ T0>

以下是使用测试客户端的简单单元测试:

import unittest
from django.test import Client

class SimpleTest(unittest.TestCase):
    def setUp(self):
        # Every test needs a client.
        self.client = Client()

    def test_details(self):
        # Issue a GET request.
        response = self.client.get('/customer/details/')

        # Check that the response is 200 OK.
        self.assertEqual(response.status_code, 200)

        # Check that the rendered context contains 5 customers.
        self.assertEqual(len(response.context['customers']), 5)

也可以看看

django.test.RequestFactory

提供了测试用例类

正常的Python单元测试类扩展了unittest.TestCase的基类。 Django提供了这个基类的一些扩展:

Hierarchy of Django unit testing classes (TestCase subclasses)

Django单元测试类的层次结构

将一个普通的unittest.TestCase转换成任何一个子类很容易:将测试的基类从unittest.TestCase更改为子类。 所有标准的Python单元测试功能都将可用,并将增加一些有用的添加,如下面各节所述。

SimpleTestCase

SimpleTestCase[source]

unittest.TestCase的子类添加了这个功能:

如果您的测试进行任何数据库查询,请使用TransactionTestCaseTestCase子类。

SimpleTestCase。 allow_database_queries T0> ¶ T1>

SimpleTestCase disallows database queries by default. 这有助于避免执行会影响其他测试的写查询,因为每个SimpleTestCase测试都不在事务中运行。 如果您不关心这个问题,可以通过在测试类中设置allow_database_queries类属性为True来禁用此行为。

警告

SimpleTestCase and its subclasses (e.g. TestCase, …) rely on setUpClass() and tearDownClass() to perform some class-wide initialization (e.g. overriding settings). 如果您需要重写这些方法,请不要忘记调用super实现:

class MyTestCase(TestCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        ...

    @classmethod
    def tearDownClass(cls):
        ...
        super().tearDownClass()

如果在setUpClass()期间发生异常,请务必说明Python的行为。 如果发生这种情况,则不会执行该类中的测试和tearDownClass() django.test.TestCase的情况下,这将泄漏在super()中创建的事务,这会导致各种症状,包括某些平台上的分段故障(macOS )。 如果您想在setUpClass()中故意引发unittest.SkipTest等异常,请务必在调用super()避免这一点。

TransactionTestCase

TransactionTestCase[source]

TransactionTestCase inherits from SimpleTestCase to add some database-specific features:

Django的TestCase类是TransactionTestCase的一个更常用的子类,它利用数据库事务工具来加速数据库在每个开始时重置为已知状态的过程测试。 然而,这样做的后果是某些数据库行为无法在Django TestCase类中进行测试。 例如,您不能像在使用select_for_update()时所要求的那样在事务内测试一个代码块。 在这些情况下,您应该使用TransactionTestCase

TransactionTestCase and TestCase are identical except for the manner in which the database is reset to a known state and the ability for test code to test the effects of commit and rollback:

  • 通过截断所有表,TransactionTestCase在测试运行后重置数据库。 A TransactionTestCase may call commit and rollback and observe the effects of these calls on the database.
  • 另一方面,TestCase在测试之后不会截断表格。 而是将测试代码封装在测试结束时回滚的数据库事务中。 这保证了测试结束时的回滚将数据库恢复到初始状态。

警告

TestCase running on a database that does not support rollback (e.g. MySQL with the MyISAM storage engine), and all instances of TransactionTestCase, will roll back at the end of the test by deleting all data from the test database.

Apps will not see their data reloaded; if you need this functionality (for example, third-party apps should enable this) you can set serialized_rollback = True inside the TestCase body.

TestCase

TestCase[source]

这是用于在Django中编写测试的最常用的类。 它继承自TransactionTestCase(以及扩展SimpleTestCase)。 如果您的Django应用程序不使用数据库,请使用SimpleTestCase

班上:

  • 在两个嵌套的atomic()块中包装测试:一个用于整个类,另一个用于每个测试。 因此,如果要测试某些特定的数据库事务行为,请使用TransactionTestCase
  • 在每个测试结束时检查可延迟的数据库约束。

它还提供了一个额外的方法:

类方法 测试用例。setUpTestData()[source]

上面描述的类级atomic块允许在类级创建初始数据,一次为整个TestCase创建。 与使用setUp()相比,这种技术允许更快的测试。

例如:

from django.test import TestCase

class MyTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Set up data for the whole TestCase
        cls.foo = Foo.objects.create(bar="Test")
        ...

    def test1(self):
        # Some test using self.foo
        ...

    def test2(self):
        # Some other test using self.foo
        ...

Note that if the tests are run on a database with no transaction support (for instance, MySQL with the MyISAM engine), setUpTestData() will be called before each test, negating the speed benefits.

小心不要修改在你的测试方法中的setUpTestData()中创建的任何对象。 在测试方法之间,对在类级完成的设置工作中的内存中对象的修改将一直存在。 例如,如果您需要修改它们,则可以使用refresh_from_db()将它们重新加载到setUp()方法中。

LiveServerTestCase

LiveServerTestCase[source]

LiveServerTestCase does basically the same as TransactionTestCase with one extra feature: it launches a live Django server in the background on setup, and shuts it down on teardown. 这允许使用Django dummy client以外的自动化测试客户端(例如Selenium客户端)在浏览器内执行一系列功能测试并模拟一个真实的用户的行为。

在线服务器监听localhost并绑定到使用操作系统分配的空闲端口的端口0。 在测试过程中,服务器的URL可以通过self.live_server_url访问。

在Django 1.11中更改:

在较早的版本中,Django尝试了一个预定义的端口范围,可以通过各种方式进行自定义,包括DJANGO_LIVE_TEST_SERVER_ADDRESS环境变量。 这被删除赞成简单的“绑定到端口0”技术。

为了演示如何使用LiveServerTestCase,我们来编写一个简单的Selenium测试。 首先,您需要将selenium软件包安装到您的Python路径中:

$ pip install selenium

Then, add a LiveServerTestCase-based test to your app’s tests module (for example: myapp/tests.py). For this example, we’ll assume you’re using the staticfiles app and want to have static files served during the execution of your tests similar to what we get at development time with DEBUG=True, i.e. without having to collect them using collectstatic. 我们将使用提供该功能的StaticLiveServerTestCase子类。 如果你不需要的话,把它替换为django.test.LiveServerTestCase

此测试的代码可能如下所示:

from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium.webdriver.firefox.webdriver import WebDriver

class MySeleniumTests(StaticLiveServerTestCase):
    fixtures = ['user-data.json']

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.selenium = WebDriver()
        cls.selenium.implicitly_wait(10)

    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()

    def test_login(self):
        self.selenium.get('%s%s' % (self.live_server_url, '/login/'))
        username_input = self.selenium.find_element_by_name("username")
        username_input.send_keys('myuser')
        password_input = self.selenium.find_element_by_name("password")
        password_input.send_keys('secret')
        self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()

最后,你可以运行测试如下:

$ ./manage.py test myapp.tests.MySeleniumTests.test_login

这个例子会自动打开Firefox,然后进入登录页面,输入凭据并按下“登录”按钮。 Selenium提供其他驱动程序,以防您安装Firefox或希望使用其他浏览器。 上面的例子只是Selenium客户端能做的一小部分;请查阅完整参考了解更多详情。

注意

使用内存中的SQLite数据库运行测试时,同一个数据库连接将由两个并行线程共享:运行服务器的线程和运行测试用例的线程。 防止两个线程通过这个共享连接同时进行数据库查询是非常重要的,因为这有时会随机地导致测试失败。 所以你需要确保两个线程不能同时访问数据库。 特别是,这意味着在某些情况下(例如,在单击链接或提交表单之后),您可能需要检查Selenium是否收到响应,并在继续执行进一步的测试之前加载下一页。 例如,通过让Selenium等待,直到在响应中找到<body> HTML标记(需要Selenium> 2.13):

def test_login(self):
    from selenium.webdriver.support.wait import WebDriverWait
    timeout = 2
    ...
    self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()
    # Wait until the response is received
    WebDriverWait(self.selenium, timeout).until(
        lambda driver: driver.find_element_by_tag_name('body'))

这里棘手的事情是,实际上没有“页面加载”这样的事情,特别是在服务器生成初始文档之后动态地生成HTML的现代Web应用程序中。 因此,仅仅检查响应中是否存在<body>可能不一定适用于所有用例。 有关更多信息,请参阅Selenium FAQSelenium文档

测试用例功能

默认测试客户端

SimpleTestCase。客户端 T0> ¶ T1>

每个测试用例都在一个 django.test。*测试用例 实例可以访问Django测试客户端的一个实例。 这个客户端可以被访问为self.client 这个客户端是为每个测试重新创建的,所以你不必担心从一个测试转移到另一个测试的状态(如cookie)。

这意味着,而不是在每个测试中实例化一个Client

import unittest
from django.test import Client

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

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

...您可以参考self.client,如下所示:

from django.test import TestCase

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

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

自定义测试客户端

SimpleTestCase。 client_class T0> ¶ T1>

如果要使用不同的Client类(例如,具有自定义行为的子类),请使用client_class类属性:

from django.test import TestCase, Client

class MyTestClient(Client):
    # Specialized methods for your environment
    ...

class MyTest(TestCase):
    client_class = MyTestClient

    def test_my_stuff(self):
        # Here self.client is an instance of MyTestClient...
        call_some_test_code()

夹具加载

TransactionTestCase。夹具 T0> ¶ T1>

如果数据库中没有任何数据,那么数据库支持的网站的测试用例就没什么用处了。 测试更具可读性,使用ORM创建对象更具可维护性,例如在TestCase.setUpTestData()中,但是,您也可以使用fixtures。

fixture是Django知道如何导入数据库的数据集合。 例如,如果您的网站拥有用户帐户,则可能会设置一个伪造的用户帐户夹具,以便在测试期间填充数据库。

创建夹具最直接的方法是使用manage.py dumpdata命令。 这假定你已经在你的数据库中有一些数据。 有关更多详细信息,请参阅dumpdata documentation

Once you’ve created a fixture and placed it in a fixtures directory in one of your INSTALLED_APPS, you can use it in your unit tests by specifying a fixtures class attribute on your django.test.TestCase subclass:

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

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

    def setUp(self):
        # Test definitions as before.
        call_setup_methods()

    def testFluffyAnimals(self):
        # A test that uses the fixtures.
        call_some_test_code()

具体会发生什么:

  • 在每次测试开始时,在运行setUp()之前,Django将刷新数据库,并在调用migrate之后直接返回数据库的状态。
  • 然后,安装所有命名的灯具。 在这个例子中,Django将安装任何名为mammals的JSON夹具,接着是任何名为birds的夹具。 有关定义和安装灯具的更多详细信息,请参阅loaddata文档。

For performance reasons, TestCase loads fixtures once for the entire test class, before setUpTestData(), instead of before each test, and it uses transactions to clean the database before each test. 在任何情况下,您都可以确定测试的结果不会受到另一个测试或测试执行顺序的影响。

默认情况下,灯具只加载到default数据库中。 如果您正在使用多个数据库并设置multi_db=True,灯具将被加载到所有数据库中。

URLconf配置

如果您的应用程序提供了视图,那么您可能需要包含使用测试客户端来执行这些视图的测试。 但是,最终用户可以自由地在应用程序的任意网址上部署视图。 这意味着您的测试不能依赖于您的视图将在特定的URL上可用的事实。 使用@override_settings(ROOT_URLCONF=...)修饰您的测试类或测试方法,以便进行URLconf配置。

多数据库支持

TransactionTestCase。 multi_db T0> ¶ T1>

Django设置一个测试数据库,对应于你的设置文件中的DATABASES定义中定义的每个数据库。 然而,调用flush消耗了运行Django TestCase所花费的大部分时间,这可以确保在每次测试运行开始时您都拥有一个干净的数据库。 如果您有多个数据库,则需要多个刷新(每个数据库一个),这可能是一个耗时的活动 - 特别是如果您的测试不需要测试多数据库活动。

作为优化,Django只在每次测试运行开始时刷新default数据库。 如果您的设置包含多个数据库,并且您有一个要求每个数据库都是干净的测试,则可以使用测试套件中的multi_db属性来请求完全刷新。

例如:

class TestMyViews(TestCase):
    multi_db = True

    def test_index_page_view(self):
        call_some_test_code()

This test case will flush all the test databases before running test_index_page_view.

The multi_db flag also affects into which databases the TransactionTestCase.fixtures are loaded. By default (when multi_db=False), fixtures are only loaded into the default database. 如果multi_db=True,灯具将加载到所有数据库中。

覆盖设置

警告

使用下面的功能暂时改变测试中的设置值。 请勿直接操作django.conf.settings,因为在这种操作之后,Django将不会恢复原始值。

SimpleTestCase。settings()[source]

出于测试目的,在运行测试代码之后暂时更改设置并恢复为原始值通常很有用。 对于这个用例,Django提供了一个名为settings()的标准Python上下文管理器(参见 PEP 343),

from django.test import TestCase

class LoginTestCase(TestCase):

    def test_login(self):

        # First check for the default behavior
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/accounts/login/?next=/sekrit/')

        # Then override the LOGIN_URL setting
        with self.settings(LOGIN_URL='/other/login/'):
            response = self.client.get('/sekrit/')
            self.assertRedirects(response, '/other/login/?next=/sekrit/')

这个例子将覆盖withLOGIN_URL设置,并将其值重置为之前的状态。

SimpleTestCase。modify_settings()[source]

重新定义包含值列表的设置可能难以实现。 实际上,添加或删除值通常就足够了。 使用modify_settings()上下文管理器可以轻松:

from django.test import TestCase

class MiddlewareTestCase(TestCase):

    def test_cache_middleware(self):
        with self.modify_settings(MIDDLEWARE={
            'append': 'django.middleware.cache.FetchFromCacheMiddleware',
            'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
            'remove': [
                'django.contrib.sessions.middleware.SessionMiddleware',
                'django.contrib.auth.middleware.AuthenticationMiddleware',
                'django.contrib.messages.middleware.MessageMiddleware',
            ],
        }):
            response = self.client.get('/')
            # ...

对于每个操作,您可以提供值列表或字符串。 当列表中的值已经存在时,appendprepend不起作用;当值不存在时,remove

override_settings()[source]

如果你想覆盖一个测试方法的设置,Django提供了override_settings()装饰器(见 PEP 318)。 它是这样使用的:

from django.test import TestCase, override_settings

class LoginTestCase(TestCase):

    @override_settings(LOGIN_URL='/other/login/')
    def test_login(self):
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/other/login/?next=/sekrit/')

装饰器也可以应用于TestCase类:

from django.test import TestCase, override_settings

@override_settings(LOGIN_URL='/other/login/')
class LoginTestCase(TestCase):

    def test_login(self):
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/other/login/?next=/sekrit/')
modify_settings()[source]

同样,Django提供了modify_settings()装饰器:

from django.test import TestCase, modify_settings

class MiddlewareTestCase(TestCase):

    @modify_settings(MIDDLEWARE={
        'append': 'django.middleware.cache.FetchFromCacheMiddleware',
        'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
    })
    def test_cache_middleware(self):
        response = self.client.get('/')
        # ...

装饰器也可以应用于测试用例类:

from django.test import TestCase, modify_settings

@modify_settings(MIDDLEWARE={
    'append': 'django.middleware.cache.FetchFromCacheMiddleware',
    'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
})
class MiddlewareTestCase(TestCase):

    def test_cache_middleware(self):
        response = self.client.get('/')
        # ...

注意

当给定一个类时,这些装饰器直接修改类并返回;他们不创建并返回它的修改副本。 所以如果你试图调整上面的例子来把返回值赋给一个不同于LoginTestCaseMiddlewareTestCase的名字,你可能会惊奇地发现原来的测试用例类是仍然同样受装饰者的影响。 对于给定的类,modify_settings()总是应用于override_settings()之后。

警告

设置文件包含一些只在Django内部初始化过程中被查询的设置。 如果使用override_settings更改它们,则通过django.conf.settings模块访问该设置时,设置将发生更改,但Django的内部版本以不同的方式访问它。 实际上,使用override_settings()modify_settings()这些设置可能不会达到您期望的效果。

We do not recommend altering the DATABASES setting. 更改CACHES设置是可能的,但是如果使用缓存的内部函数(如django.contrib.sessions),则会有点棘手。 例如,您将不得不在使用缓存会话和覆盖CACHES的测试中重新初始化会话后端。

最后,避免将你的设置作为模块级别的常量进行别名,因为override_settings()不能在这些值上工作,因为它们只在第一次导入模块时被评估。

您也可以在设置被覆盖之后通过删除设置来模拟缺少设置,如下所示:

@override_settings()
def test_something(self):
    del settings.LOGIN_URL
    ...

当覆盖设置时,请确保处理应用程序代码使用缓存或类似功能的情况,即使设置已更改,也会保留状态。 Django提供了django.test.signals.setting_changed信号,允许您注册回调来清理,或者在更改设置时复位状态。

Django本身使用这个信号来重置各种数据:

被覆盖的设置 数据重置
USE_TZ,TIME_ZONE 数据库时区
TEMPLATES 模板引擎
SERIALIZATION_MODULES 串行器缓存
LOCALE_PATHS,LANGUAGE_CODE 默认翻译和加载的翻译
MEDIA_ROOT,DEFAULT_FILE_STORAGE 默认的文件存储

清空测试发件箱

如果您使用任何Django自定义的TestCase类,则测试运行器将在每个测试用例开始时清除测试电子邮件发件箱的内容。

有关测试期间电子邮件服务的更多详细信息,请参阅下面的电子邮件服务

断言¶ T0>

As Python’s normal unittest.TestCase class implements assertion methods such as assertTrue() and assertEqual(), Django’s custom TestCase class provides a number of custom assertion methods that are useful for testing Web applications:

大多数这些断言方法给出的失败消息可以用msg_prefix参数定制。 这个字符串将会以断言产生的任何失败消息为前缀。 这使您可以提供其他详细信息,可以帮助您确定测试套件中发生故障的位置和原因。

SimpleTestCase。assertRaisesMessage(expected_exception, expected_message, callable, *args, **kwargs)[source]
SimpleTestCase。assertRaisesMessageexpected_exceptionexpected_message

断言callable的执行会引发expected_exception,并且在异常的消息中找到expected_message 任何其他结果都被报告为失败。 这是unittest.TestCase.assertRaisesRegex()的简单版本,不同之处在于expected_message不被视为正则表达式。

If only the expected_exception and expected_message parameters are given, returns a context manager so that the code being tested can be written inline rather than as a function:

with self.assertRaisesMessage(ValueError, 'invalid literal for int()'):
    int('a')
SimpleTestCase。assertFieldOutput(fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value='')[source]

声明一个表单域与各种输入正确的行为。

参数:
  • fieldclass - 要测试的字段的类。
  • 有效 - 一个将有效输入映射到其预期清理值的字典。
  • 无效 - 将无效输入映射到一个或多个引发的错误消息的字典。
  • field_args - 传递来实例化字段的参数。
  • field_kwargs - 通过实例化字段的kwargs。
  • empty_value - empty_values中输入的预期清理输出。

例如,以下代码测试EmailField接受a@a.com作为有效的电子邮件地址,但拒绝aaa信息:

self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': ['Enter a valid email address.']})
SimpleTestCase。assertFormError(response, form, field, errors, msg_prefix='')[source]

声明表单上的字段在表单上呈现提供的错误列表。

formForm实例在模板上下文中给出的名称。

field是要检查的表单上的字段的名称。 如果field的值为None,则会检查非字段错误(可通过form.non_field_errors()访问的错误)。

errors是错误字符串或错误字符串的列表,这些错误字符串是表单验证的结果。

SimpleTestCase。assertFormsetError(response, formset, form_index, field, errors, msg_prefix='')[source]

声明formset在渲染时引发提供的错误列表。

formset is the name the Formset instance was given in the template context.

form_indexFormset中表单的编号。 如果form_index的值为None,则会检查非表单错误(您可以通过formset.non_form_errors()访问的错误)。

field是要检查的表单上的字段的名称。 如果field的值为None,则会检查非字段错误(可通过form.non_field_errors()访问的错误)。

errors是错误字符串或错误字符串的列表,这些错误字符串是表单验证的结果。

SimpleTestCase。assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False)[source]

断言一个Response实例产生了给定的status_code,并且text出现在响应的内容中。 如果提供counttext在响应中必须正好发生count次。

html设置为True以将text处理为HTML。 与响应内容的比较将基于HTML语义而不是逐字符相等。 空白在大多数情况下被忽略,属性排序不重要。 有关更多详细信息,请参阅assertHTMLEqual()

SimpleTestCase。assertNotContains(response, text, status_code=200, msg_prefix='', html=False)[source]

Asserts that a Response instance produced the given status_code and that text does not appear in the content of the response.

html设置为True以将text处理为HTML。 与响应内容的比较将基于HTML语义而不是逐字符相等。 空白在大多数情况下被忽略,属性排序不重要。 有关更多详细信息,请参阅assertHTMLEqual()

SimpleTestCase。assertTemplateUsed(response, template_name, msg_prefix='', count=None)[source]

声明具有给定名称的模板用于呈现响应。

该名称是一个字符串,如'admin/index.html'

count参数是一个整数,指示模板应该被渲染的次数。 缺省是None,这意味着该模板应该呈现一次或多次。

您可以将其用作上下文管理器,如下所示:

with self.assertTemplateUsed('index.html'):
    render_to_string('index.html')
with self.assertTemplateUsed(template_name='index.html'):
    render_to_string('index.html')
SimpleTestCase。assertTemplateNotUsed(response, template_name, msg_prefix='')[source]

声明具有给定名称的模板在呈现响应时使用not

您可以像assertTemplateUsed()一样使用它作为上下文管理器。

SimpleTestCase。assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='', fetch_redirect_response=True)[source]

Asserts that the response returned a status_code redirect status, redirected to expected_url (including any GET data), and that the final page was received with target_status_code.

如果您的请求使用follow参数,则expected_urltarget_status_code将成为重定向链的最后一个点的url和status代码。

If fetch_redirect_response is False, the final page won’t be loaded. 由于测试客户端无法获取外部URL,因此如果expected_url不是您的Django应用程序的一部分,这一点特别有用。

在两个URL之间进行比较时,方案正确处理。 如果在我们重定向到的位置没有指定任何方案,则使用原始请求的方案。 如果存在,那么expected_url中的方案就是用来进行比较的方案。

SimpleTestCase。assertHTMLEqual(html1, html2, msg=None)[source]

断言字符串html1html2是相等的。 比较基于HTML语义。 比较考虑到以下几点:

  • HTML标签之前和之后的空白被忽略。
  • 所有类型的空白都被认为是等价的。
  • 所有打开的标签都隐式关闭,例如当周围的标签被关闭或HTML文档结束。
  • 空标签相当于它们的自闭版本。
  • HTML元素的属性排序不重要。
  • 没有参数的属性等于名称和值相同的属性(参见示例)。

以下示例是有效的测试,不会引发任何AssertionError

self.assertHTMLEqual(
    '<p>Hello <b>world!</p>',
    '''<p>
        Hello   <b>world! <b/>
    </p>'''
)
self.assertHTMLEqual(
    '<input type="checkbox" checked="checked" id="id_accept_terms" />',
    '<input id="id_accept_terms" type="checkbox" checked>'
)

html1html2必须是有效的HTML。 如果其中一个不能被解析,则会引发一个AssertionError

出错时输出可以用msg参数自定义。

SimpleTestCase。assertHTMLNotEqualhtml1html2msg = None[source] ¶ T6>

断言字符串html1html2不等于 比较基于HTML语义。 有关详细信息,请参阅assertHTMLEqual()

html1html2必须是有效的HTML。 如果其中一个不能被解析,则会引发一个AssertionError

出错时输出可以用msg参数自定义。

SimpleTestCase。assertXMLEqualxml1xml2msg = None[source] ¶ T6>

断言字符串xml1xml2是相等的。 该比较基于XML语义。 类似于assertHTMLEqual(),对解析的内容进行比较,因此只考虑语义差异,而不考虑语法差异。 在任何参数中传递无效的XML时,即使两个字符串都相同,也始终会引发AssertionError

出错时输出可以用msg参数自定义。

SimpleTestCase。assertXMLNotEqualxml1xml2msg = None[source] ¶ T6>

断言字符串xml1xml2不等于 该比较基于XML语义。 有关详细信息,请参阅assertXMLEqual()

出错时输出可以用msg参数自定义。

SimpleTestCase。assertInHTML(needle, haystack, count=None, msg_prefix='')[source]

断言HTML片段needle包含在haystack中。

如果指定了count整数参数,那么将严格验证needle出现次数。

在大多数情况下,空白被忽略,属性排序不重要。 传入的参数必须是有效的HTML。

SimpleTestCase。assertJSONEqualrawexpected_datamsg = None[source] ¶ T6>

断言JSON片段rawexpected_data是相等的。 当重量级委托给json库时,通常会使用JSON不重要的空白规则。

出错时输出可以用msg参数自定义。

SimpleTestCase。assertJSONNotEqualrawexpected_datamsg = None[source] ¶ T6>

断言JSON片段rawexpected_data不等于 有关更多详细信息,请参阅assertJSONEqual()

出错时输出可以用msg参数自定义。

TransactionTestCase。assertQuerysetEqual(qs, values, transform=repr, ordered=True, msg=None)[source]

断言查询集qs将返回值values的特定列表。

使用函数transform执行qsvalues内容的比较。默认情况下,这意味着比较每个值的repr() 如果repr()不提供唯一的或有帮助的比较,则可以使用任何其他可调用对象。

默认情况下,比较也是依赖于顺序的。 如果qs不提供隐式排序,则可以将ordered参数设置为False,将比较结果转换为collections.Counter 如果订单未定义(如果给定的qs没有排序并且比较是针对多个有序值),则会引发ValueError

出错时输出可以用msg参数自定义。

TransactionTestCase。assertNumQueries(num, func, *args, **kwargs)[source]

断言当用*args**kwargs调用func时,执行num数据库查询。

如果在kwargs中存在"using"键,则将其用作检查查询数量的数据库别名。 If you wish to call a function with a using parameter you can do it by wrapping the call with a lambda to add an extra parameter:

self.assertNumQueries(7, lambda: my_function(using=7))

您也可以将其用作上下文管理器:

with self.assertNumQueries(2):
    Person.objects.create(name="Aaron")
    Person.objects.create(name="Daniel")

标记测试

您可以标记您的测试,以便您可以轻松地运行特定的子集。 例如,您可能会标记快速或慢速测试:

from django.test import tag

class SampleTestCase(TestCase):

    @tag('fast')
    def test_fast(self):
        ...

    @tag('slow')
    def test_slow(self):
        ...

    @tag('slow', 'core')
    def test_slow_but_core(self):
        ...

您也可以标记测试用例:

@tag('slow', 'core')
class SampleTestCase(TestCase):
    ...

然后你可以选择运行哪些测试。 例如,只运行快速测试:

$ ./manage.py test --tag=fast

或者运行快速测试和核心测试(尽管速度很慢):

$ ./manage.py test --tag=fast --tag=core

您还可以通过标记排除测试。 运行核心测试,如果他们不慢:

$ ./manage.py test --tag=core --exclude-tag=slow

test --exclude-tag has precedence over test --tag, so if a test has two tags and you select one of them and exclude the other, the test won’t be run.

电子邮件服务

如果您的任何Django视图使用Django’s email functionality发送电子邮件,那么您可能不希望每次使用该视图运行测试时发送电子邮件。 由于这个原因,Django的测试运行器会自动将所有Django发送的邮件重定向到一个虚拟的发件箱。 这使您可以测试发送电子邮件的每个方面 - 从发送到每封邮件内容的邮件数量 - 而不实际发送邮件。

测试运行者通过用测试后端透明地替换正常的电子邮件后端来实现这一点。 (别担心,如果你正在运行的话,这对Django之外的其他邮件发送者没有任何影响,比如你的邮件服务器)。

django.core.mail。发件箱 T0> ¶ T1>

在测试运行期间,每封发出的电子邮件都保存在django.core.mail.outbox中。 这是已发送的所有EmailMessage实例的简单列表。 当使用locmem电子邮件后端时,outbox属性是only创建的特殊属性。 它通常不作为django.core.mail模块的一部分存在,因此不能直接导入。 下面的代码显示了如何正确访问这个属性。

Here’s an example test that examines django.core.mail.outbox for length and contents:

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

class EmailTest(TestCase):
    def test_send_email(self):
        # Send message.
        mail.send_mail(
            'Subject here', 'Here is the message.',
            'from@example.com', ['to@example.com'],
            fail_silently=False,
        )

        # Test that one message has been sent.
        self.assertEqual(len(mail.outbox), 1)

        # Verify that the subject of the first message is correct.
        self.assertEqual(mail.outbox[0].subject, 'Subject here')

As noted previously, the test outbox is emptied at the start of every test in a Django *TestCase. 要手动清空发件箱,请将空列表分配给mail.outbox

from django.core import mail

# Empty the test outbox
mail.outbox = []

管理命令

管理命令可以使用call_command()函数进行测试。 输出可以被重定向到一个StringIO实例中:

from io import StringIO
from django.core.management import call_command
from django.test import TestCase

class ClosepollTest(TestCase):
    def test_command_output(self):
        out = StringIO()
        call_command('closepoll', stdout=out)
        self.assertIn('Expected output', out.getvalue())

跳过测试

unittest库提供了@skipIf@skipUnless修饰器,如果您事先知道这些测试在某些情况下会失败,那么您可以跳过测试。

例如,如果您的测试需要特定的可选库才能成功,则可以使用@skipIf来修饰测试用例。 然后,测试运行人员将报告测试未被执行,为什么,而不是通过测试或完全省略测试。

为了补充这些跳转行为,Django提供了两个额外的skip装饰器。 这些装饰器不是测试通用布尔值,而是检查数据库的功能,如果数据库不支持特定的命名功能,则跳过测试。

装饰器使用字符串标识符来描述数据库功能。 该字符串对应于数据库连接要素类的属性。 请参阅django.db.backends.BaseDatabaseFeatures类以获取可用作跳过测试的基础的完整数据库功能列表。

skipIfDBFeature(*feature_name_strings)[source]

如果支持所有指定的数据库功能,则跳过装饰后的测试或TestCase

For example, the following test will not be executed if the database supports transactions (e.g., it would not run under PostgreSQL, but it would under MySQL with MyISAM tables):

class MyTests(TestCase):
    @skipIfDBFeature('supports_transactions')
    def test_transaction_behavior(self):
        # ... conditional test code
        pass
skipUnlessDBFeature(*feature_name_strings)[source]

如果任何指定的数据库特征不支持,则跳过装饰后的测试或TestCase

For example, the following test will only be executed if the database supports transactions (e.g., it would run under PostgreSQL, but not under MySQL with MyISAM tables):

class MyTests(TestCase):
    @skipUnlessDBFeature('supports_transactions')
    def test_transaction_behavior(self):
        # ... conditional test code
        pass