测试工具

Django 提供了一套在编写测试时非常有用的工具。

测试客户端

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

您可以使用测试客户端执行以下操作

  • 模拟对 URL 的 GET 和 POST 请求并观察响应 - 从低级别的 HTTP(结果头和状态代码)到页面内容。

  • 查看重定向链(如果有)并检查每一步的 URL 和状态代码。

  • 测试给定请求是否由给定的 Django 模板呈现,并且模板上下文包含某些值。

请注意,测试客户端并非旨在替代Selenium 或其他“浏览器内”框架。Django 的测试客户端具有不同的侧重点。简而言之

  • 使用 Django 的测试客户端来确定是否正在渲染正确的模板,以及是否向模板传递了正确的上下文数据。

  • 使用RequestFactory 直接测试视图函数,绕过路由和中间件层。

  • 使用像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

请注意有关测试客户端工作原理的一些重要事项

  • 测试客户端不需要 Web 服务器正在运行。事实上,即使没有运行任何 Web 服务器,它也能正常运行!这是因为它避免了 HTTP 的开销,并直接与 Django 框架进行交互。这有助于加快单元测试的运行速度。

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

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

    这是错误的

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

    测试客户端无法检索不由您的 Django 项目驱动的网页。如果您需要检索其他网页,请使用 Python 标准库模块,例如urllib

  • 要解析 URL,测试客户端将使用您的ROOT_URLCONF 设置指向的任何 URLconf。

  • 尽管上面的示例可以在 Python 交互式解释器中工作,但测试客户端的一些功能,特别是与模板相关的功能,只有在测试运行期间才可用。

    这是因为 Django 的测试运行器执行了一些“障眼法”以确定给定视图加载了哪个模板。这种“障眼法”(本质上是在内存中修补 Django 的模板系统)仅在测试运行期间发生。

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

    如果出于某种原因,您希望测试客户端执行 CSRF 检查,则可以创建一个强制执行 CSRF 检查的测试客户端实例。为此,在构造客户端时传入enforce_csrf_checks 参数

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

发出请求

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

class Client(enforce_csrf_checks=False, raise_request_exception=True, json_encoder=DjangoJSONEncoder, *, headers=None, query_params=None, **defaults)[source]

一个测试 HTTP 客户端。接受几个可以自定义行为的参数。

headers 允许您指定将在每个请求中发送的默认标头。例如,要设置User-Agent 标头

client = Client(headers={"user-agent": "curl/7.79.1"})

query_params 允许您指定将在每个请求中设置的默认查询字符串。

**defaults 中的任意关键字参数设置 WSGI环境变量。例如,要设置脚本名称

client = Client(SCRIPT_NAME="/app/")

注意

HTTP_ 前缀开头的关键字参数将设置为标头,但headers 参数应优先用于可读性。

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

enforce_csrf_checks 参数可用于测试 CSRF 保护(见上文)。

raise_request_exception 参数允许控制在请求期间引发的异常是否也应在测试中引发。默认为True

json_encoder 参数允许为post() 中描述的 JSON 序列化设置自定义 JSON 编码器。

Django 5.1 中的更改

添加了query_params 参数。

获得Client 实例后,您可以调用以下任何方法

get(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra)[source]

对提供的path 发出 GET 请求,并返回一个Response 对象,该对象在下面有说明。

query_params 字典中的键值对用于设置查询字符串。例如

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

…将导致评估一个等效于以下GET请求的请求。

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

也可以将这些参数传递到data参数中。但是,query_params更受欢迎,因为它适用于任何HTTP方法。

可以使用headers参数指定要在请求中发送的请求头。例如

>>> c = Client()
>>> c.get(
...     "/customers/details/",
...     query_params={"name": "fred", "age": 7},
...     headers={"accept": "application/json"},
... )

…将发送HTTP请求头HTTP_ACCEPT到详细信息视图,这是一种测试使用django.http.HttpRequest.accepts()方法的代码路径的好方法。

任意关键字参数设置WSGI 环境变量。例如,设置脚本名称的请求头

>>> c = Client()
>>> c.get("/", SCRIPT_NAME="/app/")

如果您已经拥有URL编码形式的GET参数,则可以使用该编码而不是使用data参数。例如,前面的GET请求也可以表示为

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

如果您提供了一个URL,该URL同时包含编码的GET数据和query_params或data参数,则这些参数将优先。

如果将follow设置为True,则客户端将遵循任何重定向,并且响应对象中将设置一个redirect_chain属性,该属性包含中间URL和状态码的元组。

如果您有一个URL /redirect_me/重定向到/next/,该URL重定向到/final/,这就是您将看到的内容

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

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

Django 5.1 中的更改

添加了query_params 参数。

post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, *, headers=None, query_params=None, **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设置为application/json,则如果data是字典、列表或元组,则使用json.dumps()对其进行序列化。默认情况下,序列化使用DjangoJSONEncoder执行,可以通过向Client提供json_encoder参数来覆盖它。这种序列化也适用于put()patch()delete()请求。

如果提供任何其他content_type(例如,用于XML有效负载的text/xml),则data的内容将按原样发送到POST请求中,并在HTTP Content-Type请求头中使用content_type

如果不为content_type提供值,则data中的值将使用multipart/form-data的内容类型进行传输。在这种情况下,data中的键值对将被编码为多部分消息,并用于创建POST数据有效负载。

要为给定的键提交多个值——例如,指定<select multiple>的选择——请为所需的键提供值作为列表或元组。例如,此data值将为名为choices的字段提交三个选定的值

{"choices": ["a", "b", "d"]}

提交文件是一个特殊情况。要发布文件,您只需提供文件字段名称作为键,以及您希望上传的文件的文件句柄作为值。例如,如果您的表单具有字段nameattachment,后者是一个FileField

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

您还可以提供任何类似文件的对象(例如,StringIOBytesIO)作为文件句柄。如果您正在上传到ImageField,则该对象需要一个name属性,该属性传递validate_image_file_extension验证器。例如

>>> from io import BytesIO
>>> img = BytesIO(
...     b"GIF89a\x01\x00\x01\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00"
...     b"\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x01\x00\x00"
... )
>>> img.name = "myimage.gif"

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

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

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

如果使用POST请求的URL包含编码的参数,则这些参数将在request.GET数据中可用。例如,如果您要发出以下请求

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

…处理此请求的视图可以查询request.POST以检索用户名和密码,并且可以查询request.GET以确定用户是否是访客。

如果将follow设置为True,则客户端将遵循任何重定向,并且响应对象中将设置一个redirect_chain属性,该属性包含中间URL和状态码的元组。

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

Django 5.1 中的更改

添加了query_params 参数。

head(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra)[source]

对提供的path发出HEAD请求,并返回一个Response对象。此方法的工作方式与Client.get()相同,包括followsecureheadersquery_paramsextra参数,但它不返回消息正文。

Django 5.1 中的更改

添加了query_params 参数。

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

对提供的 path 发起 OPTIONS 请求,并返回一个 Response 对象。这对于测试 RESTful 接口很有用。

当提供 data 时,它将用作请求体,并且 Content-Type 头将设置为 content_type

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

Django 5.1 中的更改

添加了query_params 参数。

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

对提供的 path 发起 PUT 请求,并返回一个 Response 对象。这对于测试 RESTful 接口很有用。

当提供 data 时,它将用作请求体,并且 Content-Type 头将设置为 content_type

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

Django 5.1 中的更改

添加了query_params 参数。

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

对提供的 path 发起 PATCH 请求,并返回一个 Response 对象。这对于测试 RESTful 接口很有用。

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

Django 5.1 中的更改

添加了query_params 参数。

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

对提供的 path 发起 DELETE 请求,并返回一个 Response 对象。这对于测试 RESTful 接口很有用。

当提供 data 时,它将用作请求体,并且 Content-Type 头将设置为 content_type

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

Django 5.1 中的更改

添加了query_params 参数。

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

对提供的 path 发起 TRACE 请求,并返回一个 Response 对象。这对于模拟诊断探测很有用。

与其他请求方法不同,data 不是作为关键字参数提供的,以符合 RFC 9110 第 9.3.8 节,该节规定 TRACE 请求不得具有主体。

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

Django 5.1 中的更改

添加了query_params 参数。

login(**credentials)
alogin(**credentials)

异步版本alogin()

如果您的站点使用 Django 的 身份验证系统 并且您处理用户登录,则可以使用测试客户端的 login() 方法来模拟用户登录站点的效果。

调用此方法后,测试客户端将拥有通过任何基于登录的测试所需的全部 Cookie 和会话数据,这些测试可能是视图的一部分。

credentials 参数的格式取决于您使用的哪个 身份验证后端(由您的 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() 如果凭据被接受并且登录成功,则返回 True

最后,您需要记住在使用此方法之前创建用户帐户。如上所述,测试运行程序使用测试数据库执行,默认情况下该数据库不包含任何用户。因此,生产站点上有效的用户帐户在测试条件下将无法使用。您需要在测试套件中创建用户 - 手动(使用 Django 模型 API)或使用测试夹具。请记住,如果您希望您的测试用户拥有密码,则不能通过直接设置密码属性来设置用户的密码 - 您必须使用 set_password() 函数来存储正确散列的密码。或者,您可以使用 create_user() 帮助程序方法来创建一个具有正确散列密码的新用户。

Django 5.0 中的更改

添加了 alogin() 方法。

force_login(user, backend=None)
aforce_login(user, backend=None)

异步版本aforce_login()

如果您的站点使用 Django 的 身份验证系统,您可以使用 force_login() 方法来模拟用户登录站点的效果。当测试需要用户登录并且用户如何登录的细节不重要时,请使用此方法而不是 login()

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

用户的 backend 属性将设置为 backend 参数的值(该值应为带点的 Python 路径字符串),或者如果未提供值,则设置为 settings.AUTHENTICATION_BACKENDS[0]authenticate() 函数(通常由 login() 调用)会像这样为用户添加注释。

此方法比 login() 快,因为它绕过了代价高昂的密码哈希算法。此外,您可以通过 在测试期间使用较弱的哈希器 来加快 login() 的速度。

Django 5.0 中的更改

aforce_login() 方法已添加。

logout()
alogout()

异步版本alogout()

如果您的站点使用 Django 的 身份验证系统,则可以使用 logout() 方法来模拟用户退出站点的效果。

调用此方法后,测试客户端将清除所有 cookie 和会话数据,并将它们重置为默认值。后续请求将看起来来自 AnonymousUser

Django 5.0 中的更改

alogout() 方法已添加。

测试响应

get()post() 方法都返回一个 Response 对象。此 Response 对象同于 Django 视图返回的 HttpResponse 对象;测试响应对象有一些其他数据,可用于测试代码进行验证。

具体来说,Response 对象具有以下属性

class Response
client

用于发出导致响应的请求的测试客户端。

content

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

context

用于渲染生成响应内容的模板的模板 Context 实例。

如果渲染的页面使用了多个模板,则 context 将是一个 Context 对象列表,按照渲染顺序排列。

无论渲染过程中使用了多少个模板,您都可以使用 [] 运算符检索上下文值。例如,可以使用以下方法检索上下文变量 name

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

不使用 Django 模板?

此属性仅在使用 DjangoTemplates 后端时填充。如果您使用的是其他模板引擎,则 context_data 可能是具有该属性的响应的合适替代方案。

exc_info

一个包含三个值的元组,提供有关在视图期间发生的任何未处理异常的信息。

这些值是 (type, value, traceback),与 Python 的 sys.exc_info() 返回的值相同。它们的意思是

  • type:异常的类型。

  • value:异常实例。

  • traceback:一个跟踪回溯对象,封装了异常最初发生时点的调用堆栈。

如果没有发生异常,则 exc_info 将为 None

json(**kwargs)

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

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

如果 Content-Type 标头不是 "application/json",则尝试解析响应时将引发 ValueError

request

刺激响应的请求数据。

wsgi_request

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

status_code

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

templates

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

不使用 Django 模板?

此属性仅在使用 DjangoTemplates 后端时填充。如果您使用的是其他模板引擎,则 template_name 可能是合适的替代方案,如果您只需要用于渲染的模板的名称。

resolver_match

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

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

# Class-based views need to compare the view_class, as the
# functions generated by as_view() won't be equal.
self.assertIs(response.resolver_match.func.view_class, MyView)

如果未找到给定的 URL,则访问此属性将引发 Resolver404 异常。

与普通响应一样,您还可以通过 HttpResponse.headers 访问标头。例如,您可以使用 response.headers['Content-Type'] 确定响应的内容类型。

异常

如果将测试客户端指向引发异常的视图,并且Client.raise_request_exceptionTrue,则该异常将在测试用例中可见。然后,您可以使用标准的try ... except块或assertRaises()来测试异常。

测试客户端无法看到的唯一异常是Http404PermissionDeniedSystemExitSuspiciousOperation。Django 在内部捕获这些异常并将它们转换为相应的 HTTP 响应代码。在这些情况下,您可以在测试中检查response.status_code

如果Client.raise_request_exceptionFalse,则测试客户端将返回 500 响应,就像返回到浏览器一样。该响应具有属性exc_info,用于提供有关未处理异常的信息。

持久状态

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

不会遵循这些 cookie 的过期策略。如果希望 cookie 过期,请手动删除它或创建一个新的Client实例(这将有效地删除所有 cookie)。

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

Client.cookies

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

Client.session

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

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

def test_something(self):
    session = self.client.session
    session["somekey"] = "test"
    session.save()
Client.asession()
Django 5.0 中的新功能。

这与session属性类似,但它在异步上下文中工作。

设置语言

在测试支持国际化和本地化的应用程序时,您可能希望为测试客户端请求设置语言。执行此操作的方法取决于是否启用了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-LanguageHTTP 标头。

def test_language_using_header(self):
    response = self.client.get("/", headers={"accept-language": "fr"})
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

注意

使用这些方法时,请确保在每个测试结束时重置活动语言。

def tearDown(self):
    translation.activate(settings.LANGUAGE_CODE)

更多详细信息请参见Django 如何发现语言偏好

如果未启用中间件,则可以使用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.")

更多详细信息请参见显式设置活动语言

示例

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

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)

提供的测试用例类

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

Hierarchy of Django unit testing classes (TestCase subclasses)

Django 单元测试类的层次结构

您可以将普通的unittest.TestCase转换为任何子类:将测试的基类从unittest.TestCase更改为子类。所有标准的 Python 单元测试功能都可用,并且会通过下面每个部分中描述的一些有用补充进行增强。

SimpleTestCase

class SimpleTestCase[source]

unittest.TestCase的一个子类,它添加了以下功能。

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

SimpleTestCase.databases

SimpleTestCase默认情况下不允许数据库查询。这有助于避免执行写入查询,因为这些查询会影响其他测试,因为每个SimpleTestCase测试都不会在事务中运行。如果您不关心此问题,可以通过在测试类上将databases类属性设置为'__all__'来禁用此行为。

警告

SimpleTestCase 及其子类(例如 TestCase 等)依赖于 setUpClass()tearDownClass() 来执行一些类级别的初始化操作(例如覆盖设置)。如果您需要覆盖这些方法,请不要忘记调用 super 实现。

class MyTestCase(TestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        ...

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

请务必考虑 Python 在 setUpClass() 中引发异常时的行为。如果发生这种情况,则该类中的测试和 tearDownClass() 都不会运行。在 django.test.TestCase 的情况下,这将导致在 super() 中创建的事务泄漏,从而导致各种问题,包括在某些平台(例如 macOS)上出现段错误。如果您想在 setUpClass() 中有意引发异常,例如 unittest.SkipTest,请确保在调用 super() 之前执行此操作,以避免此问题。

TransactionTestCase

class TransactionTestCase[source]

TransactionTestCase 继承自 SimpleTestCase 以添加一些特定于数据库的功能。

Django 的 TestCase 类是 TransactionTestCase 的一个更常用的子类,它利用数据库事务功能来加快在每次测试开始时将数据库重置为已知状态的过程。但是,其结果是,某些数据库行为无法在 Django TestCase 类中进行测试。例如,您无法测试代码块是否在事务中执行,这在使用 select_for_update() 时是必需的。在这些情况下,您应该使用 TransactionTestCase

TransactionTestCaseTestCase 除了数据库重置为已知状态的方式以及测试代码测试提交和回滚效果的能力之外,其他方面都相同。

  • TransactionTestCase 通过截断所有表来在测试运行后重置数据库。 TransactionTestCase 可以调用 commit 和 rollback 并观察这些调用对数据库的影响。

  • 另一方面,TestCase 在测试后不会截断表。相反,它将测试代码包含在一个数据库事务中,该事务在测试结束时回滚。这保证了测试结束时的回滚将数据库恢复到其初始状态。

警告

在不支持回滚的数据库(例如使用 MyISAM 存储引擎的 MySQL)上运行的 TestCase,以及所有 TransactionTestCase 实例,都将在测试结束时通过删除测试数据库中的所有数据来回滚。

应用程序 将不会看到其数据重新加载;如果您需要此功能(例如,第三方应用程序应启用此功能),可以在 TestCase 主体中设置 serialized_rollback = True

TestCase

class TestCase[source]

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

该类

  • 将测试包装在两个嵌套的 atomic() 块中:一个用于整个类,一个用于每个测试。因此,如果您想测试某些特定的数据库事务行为,请使用 TransactionTestCase

  • 在每个测试结束时检查可延迟数据库约束。

它还提供了一个附加方法

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

请注意,如果测试在没有事务支持的数据库(例如使用 MyISAM 引擎的 MySQL)上运行,则 setUpTestData() 将在每个测试之前调用,从而抵消速度优势。

setUpTestData() 中分配给类属性的对象必须支持使用 copy.deepcopy() 创建深拷贝,以便将它们与每个测试方法执行的更改隔离开。

classmethod TestCase.captureOnCommitCallbacks(using=DEFAULT_DB_ALIAS, execute=False)[source]

返回一个上下文管理器,该管理器捕获给定数据库连接的 transaction.on_commit() 回调。它返回一个列表,该列表在上下文退出时包含捕获的回调函数。从此列表中,您可以对回调进行断言或调用它们以调用其副作用,从而模拟提交。

using 是要捕获回调的数据库连接的别名。

如果 executeTrue,则如果未发生异常,则上下文管理器退出时将调用所有回调。这模拟了代码块包装后执行的提交。

例如

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


class ContactTests(TestCase):
    def test_post(self):
        with self.captureOnCommitCallbacks(execute=True) as callbacks:
            response = self.client.post(
                "/contact/",
                {"message": "I like your site"},
            )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(callbacks), 1)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].subject, "Contact Form")
        self.assertEqual(mail.outbox[0].body, "I like your site")

LiveServerTestCase

class LiveServerTestCase[source]

LiveServerTestCase 基本上与 TransactionTestCase 相同,但多了一个功能:它在设置时在后台启动一个活动的 Django 服务器,并在拆卸时将其关闭。这允许使用除 Django 虚拟客户端 之外的自动化测试客户端,例如 Selenium 客户端,在浏览器中执行一系列功能测试并模拟真实用户的操作。

活动服务器监听 localhost 并绑定到端口 0,该端口使用操作系统分配的空闲端口。在测试期间,可以使用 self.live_server_url 访问服务器的 URL。

为了演示如何使用 LiveServerTestCase,让我们编写一个 Selenium 测试。首先,您需要安装 selenium

$ python -m pip install "selenium >= 4.8.0"
...\> py -m pip install "selenium >= 4.8.0"

然后,将一个基于 LiveServerTestCase 的测试添加到应用程序的测试模块中(例如:myapp/tests.py)。在本例中,我们假设您正在使用 staticfiles 应用程序,并且希望在测试执行期间提供静态文件服务,类似于我们在开发时使用 DEBUG=True 时获得的服务,即无需使用 collectstatic 收集它们。我们将使用 StaticLiveServerTestCase 子类,它提供了此功能。如果您不需要此功能,请将其替换为 django.test.LiveServerTestCase

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

from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium.webdriver.common.by import By
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(f"{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
...\> manage.py test myapp.tests.MySeleniumTests.test_login

此示例将自动打开 Firefox,然后转到登录页面,输入凭据并按下“登录”按钮。如果您没有安装 Firefox 或希望使用其他浏览器,Selenium 提供其他驱动程序。以上示例只是 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")
    )

这里棘手的地方在于,实际上并没有所谓的“页面加载”,尤其是在现代 Web 应用程序中,这些应用程序在服务器生成初始文档后动态生成 HTML。因此,检查响应中是否存在 <body> 可能不一定适合所有用例。请参阅 Selenium 常见问题解答Selenium 文档 以获取更多信息。

测试用例特性

默认测试客户端

SimpleTestCase.client

django.test.*TestCase 实例中的每个测试用例都可以访问 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

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

from django.test import Client, TestCase


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

如果数据库中没有任何数据,则用于数据库支持网站的测试用例类就没有多大用处。测试更具可读性,并且使用 ORM 创建对象(例如在 TestCase.setUpTestData() 中)更容易维护,但是,您也可以使用 夹具

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

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

创建夹具并将其放置在 INSTALLED_APPS 之一中的 fixtures 目录中后,您可以通过在 django.test.TestCase 子类上指定 fixtures 类属性在单元测试中使用它

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 test_fluffy_animals(self):
        # A test that uses the fixtures.
        call_some_test_code()

以下是将要发生的事情

  • 在每个测试开始时,在运行 setUp() 之前,Django 将刷新数据库,将数据库恢复到 migrate 调用后立即的状态。

  • 然后,安装所有命名的夹具。在本例中,Django 将安装任何名为 mammals 的 JSON 夹具,然后安装任何名为 birds 的夹具。有关定义和安装夹具的更多详细信息,请参阅 夹具 主题。

出于性能原因,TestCase 为整个测试类加载一次夹具,在 setUpTestData() 之前,而不是在每个测试之前,并且它使用事务在每个测试之前清理数据库。在任何情况下,您都可以确定测试的结果不会受到其他测试或测试执行顺序的影响。

默认情况下,夹具仅加载到 default 数据库中。如果您使用多个数据库并设置 TransactionTestCase.databases,则夹具将加载到所有指定的数据库中。

URLconf 配置

如果您的应用程序提供视图,您可能希望包含使用测试客户端来执行这些视图的测试。但是,最终用户可以自由地在他们选择的任何 URL 上部署应用程序中的视图。这意味着您的测试不能依赖于您的视图将在特定 URL 上可用这一事实。使用 @override_settings(ROOT_URLCONF=...) 装饰您的测试类或测试方法以进行 URLconf 配置。

多数据库支持

TransactionTestCase.databases

Django 为设置中 DATABASES 定义中定义的每个数据库以及至少一个测试通过 databases 引用它设置一个测试数据库。

但是,运行 Django TestCase 所花费的大部分时间都消耗在对 flush 的调用上,该调用确保您在每次测试运行开始时都有一个干净的数据库。如果您有多个数据库,则需要多次刷新(每个数据库一次),这可能是一个耗时的活动——尤其是在您的测试不需要测试多数据库活动的情况下。

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

例如

class TestMyViews(TransactionTestCase):
    databases = {"default", "other"}

    def test_index_page_view(self):
        call_some_test_code()

此测试用例类将在运行 test_index_page_view 之前刷新 defaultother 测试数据库。您还可以使用 '__all__' 指定必须刷新所有测试数据库。

databases 标志还控制着 TransactionTestCase.fixtures 加载到的数据库。默认情况下,fixture 仅加载到 default 数据库中。

针对不在 databases 中的数据库的查询将给出断言错误,以防止测试之间出现状态泄漏。

TestCase.databases

默认情况下,只有 default 数据库将在 TestCase 执行期间被包装在一个事务中,并且尝试查询其他数据库将导致断言错误,以防止测试之间出现状态泄漏。

使用测试类上的 databases 类属性来请求针对非 default 数据库的事务包装。

例如

class OtherDBTests(TestCase):
    databases = {"other"}

    def test_other_db_query(self): ...

此测试仅允许针对 other 数据库进行查询。与 SimpleTestCase.databasesTransactionTestCase.databases 一样,可以使用 '__all__' 常量来指定测试应该允许对所有数据库进行查询。

覆盖设置

警告

使用下面的函数在测试中临时更改设置的值。不要直接操作 django.conf.settings,因为 Django 不会在这样的操作之后恢复原始值。

SimpleTestCase.settings()[source]

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

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/")

此示例将覆盖 LOGIN_URL 设置以用于 with 块中的代码,并在之后将其值重置为先前的状态。

SimpleTestCase.modify_settings()[source]

重新定义包含值列表的设置可能会很麻烦。在实践中,添加或删除值通常就足够了。Django 提供了 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(**kwargs)[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(*args, **kwargs)[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() 使用这些设置可能不会达到您预期的效果。

我们不建议更改 DATABASES 设置。更改 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

模板引擎

FORM_RENDERER

默认渲染器

SERIALIZATION_MODULES

序列化程序缓存

LOCALE_PATHS,LANGUAGE_CODE

默认翻译和加载的翻译

STATIC_ROOT,STATIC_URL,STORAGES

存储配置

Django 5.1 中的更改

添加了更改 FORM_RENDERER 设置时重置默认渲染器。

隔离应用程序

utils.isolate_apps(*app_labels, attr_name=None, kwarg_name=None)

将包装的上下文中定义的模型注册到它们自己的隔离 apps 注册表中。此功能在为测试创建模型类时很有用,因为类将在之后被干净地删除,并且没有名称冲突的风险。

隔离注册表应包含的应用程序标签必须作为单独的参数传递。您可以使用 isolate_apps() 作为装饰器或上下文管理器。例如

from django.db import models
from django.test import SimpleTestCase
from django.test.utils import isolate_apps


class MyModelTests(SimpleTestCase):
    @isolate_apps("app_label")
    def test_model_definition(self):
        class TestModel(models.Model):
            pass

        ...

… 或者

with isolate_apps("app_label"):

    class TestModel(models.Model):
        pass

    ...

装饰器形式也可以应用于类。

可以指定两个可选的关键字参数

  • attr_name:如果用作类装饰器,则将隔离注册表分配给该属性。

  • kwarg_name:如果用作函数装饰器,则将隔离注册表作为关键字参数传递。

用作类装饰器时,可以通过使用 attr_name 参数将用于隔离模型注册的临时 Apps 实例作为属性检索

@isolate_apps("app_label", attr_name="apps")
class TestModelDefinition(SimpleTestCase):
    def test_model_definition(self):
        class TestModel(models.Model):
            pass

        self.assertIs(self.apps.get_model("app_label", "TestModel"), TestModel)

… 或者作为方法装饰器上的参数,通过使用 kwarg_name 参数检索

class TestModelDefinition(SimpleTestCase):
    @isolate_apps("app_label", kwarg_name="apps")
    def test_model_definition(self, apps):
        class TestModel(models.Model):
            pass

        self.assertIs(apps.get_model("app_label", "TestModel"), TestModel)

清空测试收件箱

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

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

断言

由于 Python 的标准 unittest.TestCase 类实现了诸如 assertTrue()assertEqual() 等断言方法,Django 的自定义 TestCase 类提供了一些自定义的断言方法,这些方法对于测试 Web 应用程序非常有用。

大多数这些断言方法给出的失败消息可以通过 msg_prefix 参数进行自定义。此字符串将作为断言生成的任何失败消息的前缀。这允许您提供其他详细信息,这些详细信息可能有助于您识别测试套件中失败的位置和原因。

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

断言 callable 的执行引发了 expected_exception,并且异常消息中包含 expected_message。任何其他结果都将报告为失败。它是 unittest.TestCase.assertRaisesRegex() 的一个更简单的版本,区别在于 expected_message 不被视为正则表达式。

如果只提供了 expected_exceptionexpected_message 参数,则返回一个上下文管理器,以便可以将被测试的代码内联编写,而不是作为函数编写。

with self.assertRaisesMessage(ValueError, "invalid literal for int()"):
    int("a")
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message, callable, *args, **kwargs)[source]
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message)

类似于 SimpleTestCase.assertRaisesMessage(),但适用于 assertWarnsRegex() 而不是 assertRaisesRegex()

SimpleTestCase.assertFieldOutput(fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value='')[source]

断言表单字段在各种输入下表现正确。

参数:
  • fieldclass – 要测试的字段的类。

  • valid – 一个字典,将有效的输入映射到其预期的清理后的值。

  • invalid – 一个字典,将无效的输入映射到一个或多个引发的错误消息。

  • field_args – 传递给实例化字段的参数。

  • field_kwargs – 传递给实例化字段的关键字参数。

  • empty_valueempty_values 中输入的预期清理输出。

例如,以下代码测试 EmailField 接受 a@a.com 作为有效的电子邮件地址,但拒绝 aaa 并显示合理的错误消息。

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

断言表单上的某个字段引发了提供的错误列表。

form 是一个 Form 实例。表单必须是 绑定的,但不必经过验证(assertFormError() 将自动在表单上调用 full_clean())。

field 是要检查的表单上字段的名称。要检查表单的 非字段错误,请使用 field=None

errors 是字段预期具有的所有错误字符串的列表。如果只期望一个错误,您也可以传递单个错误字符串,这意味着 errors='error message' 等价于 errors=['error message']

SimpleTestCase.assertFormSetError(formset, form_index, field, errors, msg_prefix='')[source]

断言 formset 在渲染时引发了提供的错误列表。

formset 是一个 FormSet 实例。表单集必须是绑定的,但不必经过验证(assertFormSetError() 将自动在表单集上调用 full_clean())。

form_indexFormSet 中表单的编号(从 0 开始)。使用 form_index=None 检查表单集的非表单错误,即调用 formset.non_form_errors() 时出现的错误。在这种情况下,您还必须使用 field=None

fielderrors 与传递给 assertFormError() 的参数具有相同的含义。

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

断言生成的 response 具有给定的 status_code,并且 text 出现在其 content 中。如果提供了 count,则 text 必须在响应中恰好出现 count 次。

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

Django 5.1 中的更改

在旧版本中,错误消息不包含响应内容。

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

断言生成的 response 具有给定的 status_code,并且 text *不* 出现在其 content 中。

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

Django 5.1 中的更改

在旧版本中,错误消息不包含响应内容。

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

断言在渲染响应时使用了具有给定名称的模板。

response 必须是 test client 返回的响应实例。

template_name 应为字符串,例如 '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]

断言在渲染响应时*未*使用具有给定名称的模板。

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

SimpleTestCase.assertURLEqual(url1, url2, msg_prefix='')[source]

断言两个 URL 相同,忽略查询字符串参数的顺序,但名称相同的参数除外。例如,/path/?x=1&y=2 等于 /path/?y=2&x=1,但 /path/?a=1&a=2 不等于 /path/?a=2&a=1

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

断言 response 返回了 status_code 重定向状态,重定向到 expected_url(包括任何 GET 数据),并且最终页面以 target_status_code 接收。

如果您的请求使用了 follow 参数,则 expected_urltarget_status_code 将是重定向链最终点的 URL 和状态代码。

如果 fetch_redirect_responseFalse,则不会加载最终页面。由于测试客户端无法获取外部 URL,因此当 expected_url 不属于您的 Django 应用程序的一部分时,这特别有用。

在比较两个 URL 时,方案将被正确处理。如果在重定向到的位置没有指定任何方案,则使用原始请求的方案。如果存在,则 expected_url 中的方案将用于进行比较。

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

断言字符串 html1html2 相等。比较基于 HTML 语义。比较考虑以下事项

  • HTML 标签前后的空格将被忽略。

  • 所有类型的空格都被视为等效。

  • 所有打开的标签都会隐式关闭,例如当周围的标签关闭或 HTML 文档结束时。

  • 空标签等效于其自闭合版本。

  • HTML 元素属性的顺序不重要。

  • 没有参数的布尔属性(如 checked)等效于名称和值相等的属性(请参阅示例)。

  • 引用相同字符的文本、字符引用和实体引用是等效的。

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

self.assertHTMLEqual(
    "<p>Hello <b>&#x27;world&#x27;!</p>",
    """<p>
        Hello   <b>&#39;world&#39;! </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.assertHTMLNotEqual(html1, html2, msg=None)[source]

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

html1html2 必须包含 HTML 代码。如果其中一个无法解析,则会引发 AssertionError

错误情况下的输出可以通过 msg 参数自定义。

SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None)[source]

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

XML 声明、文档类型、处理指令和注释将被忽略。只比较根元素及其子元素。

错误情况下的输出可以通过 msg 参数自定义。

SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None)[source]

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

错误情况下的输出可以通过 msg 参数自定义。

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

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

如果指定了 count 整数参数,则还会严格验证 needle 出现的次数。

在大多数情况下,空格会被忽略,属性顺序也不重要。有关更多详细信息,请参阅 assertHTMLEqual()

Django 5.1 中的更改

在旧版本中,错误消息不包含 haystack

SimpleTestCase.assertNotInHTML(needle, haystack, msg_prefix='')[source]
Django 5.1 中新增。

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

在大多数情况下,空格会被忽略,属性顺序也不重要。有关更多详细信息,请参阅 assertHTMLEqual()

SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None)[source]

断言 JSON 片段 rawexpected_data 相等。通常的 JSON 非重要空格规则适用,因为主要工作委托给了 json 库。

错误情况下的输出可以通过 msg 参数自定义。

SimpleTestCase.assertJSONNotEqual(raw, expected_data, msg=None)[source]

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

错误情况下的输出可以通过 msg 参数自定义。

TransactionTestCase.assertQuerySetEqual(qs, values, transform=None, ordered=True, msg=None)[source]

断言查询集 qs 与特定的可迭代值 values 匹配。

如果提供了 transform,则 values 将与通过将 transform 应用于 qs 的每个成员而生成的列表进行比较。

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

错误情况下的输出可以通过 msg 参数自定义。

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

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

如果 kwargs 中存在 "using" 键,则将其用作要检查查询次数的数据库别名。

self.assertNumQueries(7, using="non_default_db")

如果您希望使用 using 参数调用函数,可以通过使用 lambda 包装调用来添加额外的参数。

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

子类继承超类的标签,方法继承其类的标签。假设

@tag("foo")
class SampleTestCaseChild(SampleTestCase):
    @tag("bar")
    def test(self): ...

SampleTestCaseChild.test 将被标记为 'slow''core''bar''foo'

然后您可以选择要运行的测试。例如,仅运行快速测试

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

或者运行快速测试和核心测试(即使它很慢)

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

您还可以按标签排除测试。如果核心测试不是慢速测试,则运行核心测试

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

test --exclude-tag 优先于 test --tag,因此,如果一个测试有两个标签,并且您选择其中一个并排除另一个,则该测试将不会运行。

测试异步代码

如果您只是想测试异步视图的输出,标准测试客户端将在其自己的异步循环中运行它们,而无需您进行任何额外操作。

但是,如果您想为 Django 项目编写完全异步的测试,则需要考虑几个方面。

首先,您的测试必须是测试类上的 async def 方法(以便为它们提供异步上下文)。Django 会自动检测任何 async def 测试并将其包装起来,以便它们在自己的事件循环中运行。

如果您从异步函数进行测试,则还必须使用异步测试客户端。它可以作为 django.test.AsyncClient 或任何测试上的 self.async_client 使用。

class AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, *, headers=None, query_params=None, **defaults)[source]

AsyncClient 具有与同步(普通)测试客户端相同的方法和签名,但以下例外情况除外

  • 在初始化中,defaults 中的任意关键字参数将直接添加到 ASGI 范围中。

  • 作为 extra 关键字参数传递的标头不应该具有同步客户端所需的 HTTP_ 前缀(参见 Client.get())。例如,以下是如何设置 HTTP Accept 标头

    >>> c = AsyncClient()
    >>> c.get("/customers/details/", {"name": "fred", "age": 7}, ACCEPT="application/json")
    
Django 5.0 中的更改

follow 参数的支持已添加到 AsyncClient 中。

Django 5.1 中的更改

添加了query_params 参数。

使用 AsyncClient,任何发出请求的方法都必须等待。

async def test_my_thing(self):
    response = await self.async_client.get("/some-url/")
    self.assertEqual(response.status_code, 200)

异步客户端还可以调用同步视图;它通过 Django 的 异步请求路径 运行,该路径同时支持两者。通过 AsyncClient 调用的任何视图都将为其 request 获取 ASGIRequest 对象,而不是普通客户端创建的 WSGIRequest

警告

如果您使用测试装饰器,则它们必须与异步兼容才能确保它们正常工作。Django 的内置装饰器将按预期工作,但第三方装饰器可能看起来没有执行(它们将“包装”执行流程的错误部分,而不是您的测试)。

如果您需要使用这些装饰器,那么您应该在其中使用 async_to_sync() 装饰您的测试方法。

from asgiref.sync import async_to_sync
from django.test import TestCase


class MyTests(TestCase):
    @mock.patch(...)
    @async_to_sync
    async def test_my_thing(self): ...

电子邮件服务

如果您的任何 Django 视图使用 Django 的电子邮件功能 发送电子邮件,您可能不希望每次使用该视图运行测试时都发送电子邮件。出于这个原因,Django 的测试运行程序会自动将所有 Django 发送的电子邮件重定向到一个虚拟的收件箱。这使您能够测试发送电子邮件的各个方面——从发送的消息数量到每条消息的内容——而无需实际发送消息。

测试运行程序通过透明地用测试后端替换正常的电子邮件后端来实现这一点。(不用担心——如果运行了邮件服务器,这不会影响 Django 之外的任何其他电子邮件发送程序,例如您机器的邮件服务器。)

django.core.mail.outbox

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

这是一个检查 django.core.mail.outbox 的长度和内容的测试示例

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")

前面 所述,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", poll_ids=[1], stdout=out)
        self.assertIn('Successfully closed poll "1"', out.getvalue())

跳过测试

unittest 库提供了 @skipIf@skipUnless 装饰器,允许您在事先知道某些测试在特定条件下会失败时跳过这些测试。

例如,如果您的测试需要一个特定的可选库才能成功,您可以使用 @skipIf 装饰测试用例。然后,测试运行程序将报告测试未执行以及原因,而不是使测试失败或完全省略测试。

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

装饰器使用字符串标识符来描述数据库功能。此字符串对应于数据库连接功能类的属性。有关可用作跳过测试依据的数据库功能的完整列表,请参见 django.db.backends.base.features.BaseDatabaseFeatures 类

skipIfDBFeature(*feature_name_strings)[source]

如果所有命名的数据库功能都受支持,则跳过已装饰的测试或 TestCase

例如,如果数据库支持事务,则以下测试将不会执行(例如,它不会在 PostgreSQL 下运行,但在使用 MyISAM 表的 MySQL 下会运行)

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

如果任何命名的数据库功能不受支持,则跳过已装饰的测试或 TestCase

例如,以下测试仅在数据库支持事务时才会执行(例如,它将在 PostgreSQL 下运行,但在使用 MyISAM 表的 MySQL 下不会运行)

class MyTests(TestCase):
    @skipUnlessDBFeature("supports_transactions")
    def test_transaction_behavior(self):
        # ... conditional test code
        pass
返回顶部