测试工具Link to this heading

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

测试客户端Link to this heading

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

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

  • 模拟 URL 上的 GET 和 POST 请求并观察响应——从低级 HTTP(结果头和状态码)到页面内容,应有尽有。

  • 查看重定向链(如果有的话),并检查每个步骤的 URL 和状态码。

  • 测试给定的请求是否由给定的包含某些值以及模板上下文的 Django 模板渲染。

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

  • 使用 Django 的测试客户端来确定要渲染的模板正确,并且模板已传递了正确的上下文数据。

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

  • 使用像 Selenium 这样的浏览器框架来测试 渲染的 HTML 和网页的 行为,即 JavaScript 功能。Django 还为这些框架提供了特殊的支持;请参阅 LiveServerTestCase 部分以获取更多详细信息。

一个全面的测试套件应该使用所有这些测试类型的组合。

概述和一个简单的例子Link to this heading

要使用测试客户端,请实例化 django.test.Client 并获取 web 页面:

Python console
>>> 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 的 路径,而不是整个域名。例如,这是正确的:

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

    这是不正确的:

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

    测试客户端无法获取不由你的 Django 项目提供支持的网页。如果你需要获取其他网页,使用 Python 标准库模块如 urllib。

  • 为了解析 URL,测试客户端使用你的 ROOT_URLCONF 配置指向的任何 URLconf。

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

    原因是 Django 的测试运行器为了确定哪个模板被给定的视图加载,执行了一点黑魔法。这个黑魔法(本质上是内存中 Django 模板系统的补丁)只发生在测试运行期间。

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

    如果出于某种原因,你 想要 测试客户端执行 CSRF 检查,你可以创建一个强制执行 CSRF 检查的测试客户端实例。要做到这一点,构建客户端时传入 enforce_csrf_checks 参数:

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

发出请求Link to this heading

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

class Client(enforce_csrf_checks=False, raise_request_exception=True, json_encoder=DjangoJSONEncoder, *, headers=None, query_params=None, **defaults)Link to this definition

一个用于测试的 HTTP 客户端。接受多个参数,可以自定义其行为。

headers 允许你指定默认头信息,这些头信息将与每个请求一起发送。例如,要设置一个 User-Agent 头信息:

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

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

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

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

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

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

raise_request_exception 参数允许控制是否在请求过程中引出的异常也应该在测试中引出。默认值为 True。

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

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

get(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra)Link to this definition

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

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

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

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

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

也可以将这些参数传递给 data 参数。但是,query_params 是首选,因为它适用于任何 HTTP 方法。

headers 参数可用于指定要在请求中发送的头信息。例如:

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

......会将 HTTP 头 HTTP_ACCEPT 发送到 detail 视图,这是测试使用 django.http.HttpRequest.accepts() 方法的代码路径的好方法。

任意关键字参数设置了 WSGI environ 变量。例如,设置脚本名称的头信息:

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

如果你已经有了 URL 编码形式的 GET 参数,你可以使用该编码而不是使用 data 参数。例如,之前的 GET 请求也可以这样表示:

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

如果你提供的 URL 同时包含编码的 GET 数据和 query_params 或 data 参数,这些参数将优先。

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

如果你有一个 URL /redirect_me/,它重定向到 /next/,然后重定向到 /final/,你会看到以下内容:

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

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

post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, *, headers=None, query_params=None, **extra)Link to this definition

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

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

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

...将导致对该 URL 发起 POST 请求的评估:

Text
/login/

...使用以下 POST 数据:

Text
name=fred&passwd=secret

如果你提供 application/json 为 content_type,则如果 data 是一个字典、列表或元组时,使用 json.dumps() 进行序列化。序列化默认是通过 DjangoJSONEncoder,可以通过为 Client 提供 json_encoder 参数覆盖。这个序列化也会发生在 put()、patch() 和 delete() 请求中。

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

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

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

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

提交文件是一个特殊情况。要提交文件,你只需提供文件字段名称作为键,以及要上传的文件的文件句柄作为值。例如,如果你的表单有字段 name 和 attachment,后者是一个 FileField:

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

你也可以提供任何类似文件的对象(例如:StringIO 或 BytesIO)作为文件句柄。如果你要上传到一个 ImageField,这个对象需要一个 name 属性,以通过 validate_image_file_extension 验证器。例如:

Python console
>>> 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 (读取二进制)模式打开文件。

headers、query_params 和 extra 参数的作用与 Client.get() 相同。

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

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

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

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

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

head(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra)Link to this definition

对提供的 path 发出 HEAD 请求并返回一个 Response 对象。此方法与 Client.get() 类似,包括 follow、secure、headers、query_params 和 extra 参数,只是它不返回消息正文。

options(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)Link to this definition

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

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

follow、secure、headers、query_params 和 extra 参数的作用与 Client.get() 相同。

put(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)Link to this definition

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

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

follow、secure、headers、query_params 和 extra 参数的作用与 Client.get() 相同。

patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)Link to this definition

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

follow、secure、headers、query_params 和 extra 参数的作用与 Client.get() 相同。

delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)Link to this definition

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

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

follow、secure、headers、query_params 和 extra 参数的作用与 Client.get() 相同。

trace(path, follow=False, secure=False, *, headers=None, query_params=None, **extra)Link to this definition

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

与其他请求方法不同,data 不作为关键字参数提供,以遵守 RFC 9110 Section 9.3.8,该规定要求 TRACE 请求不得包含主体。

follow、secure、headers、query_params 和 extra 参数的作用与 Client.get() 相同。

login(**credentials)Link to this definition
alogin(**credentials)Link to this definition

异步版本:alogin()

如果你的网站使用了 Django 的 认证系统,并且你需要处理登录用户的问题,你可以使用测试客户端的 login() 方法来模拟用户登录网站的效果。

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

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

Python console
>>> 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() 辅助方法来创建一个具有正确哈希密码的新用户。

force_login(user, backend=None)Link to this definition
aforce_login(user, backend=None)Link to this definition

异步版本:aforce_login()

如果你的网站使用了 Django 的 认证系统,你可以使用 force_login() 方法来模拟用户登录网站的效果。当测试需要用户登录,而用户如何登录的细节并不重要时,可以使用这个方法代替 login()。

与 login() 不同的是,这个方法跳过了认证和验证步骤:不活跃的用户(is_active=False)被允许登录,并且不需要提供用户凭证。

用户的 backend 属性将被设置为 backend 参数的值(应该是一个点分隔 Python 路径字符串),如果没有提供值,则设置为 settings.AUTHENTICATION_BACKENDS[0]。login() 调用的 authenticate() 函数通常会对用户进行注释。

这个方法比 login() 快,因为它绕过了昂贵的密码散列算法。另外,你也可以通过 在测试时使用较弱的哈希算法 来加快 login() 速度。

logout()Link to this definition
alogout()Link to this definition

异步版本:alogout()

如果你的网站使用了 Django 的 认证系统,logout() 方法可以用来模拟用户注销网站的效果。

调用此方法后,测试客户端的所有 cookie 和会话数据都会被清除为默认值。随后的请求将看起来来自一个 AnonymousUser。

测试响应Link to this heading

get() 和 post() 方法都会返回一个 Response 对象,这个 Response 对象与 Django 视图返回的 HttpResponse 对象是 不 一样的;测试响应对象有一些额外的数据,对测试代码验证很有用。

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

class ResponseLink to this definition
clientLink to this definition

用于发出请求并得到响应的测试客户端。

contentLink to this definition

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

contextLink to this definition

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

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

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

Python console
>>> response = client.get("/foo/")
>>> response.context["name"]
'Arthur'
exc_infoLink to this definition

一个由三个值组成的元组,它提供了关于在视图期间发生的未处理异常(如果有)的信息。

值是(type,value,traceback),与 Python 的 sys.exc_info() 返回的值相同。它们的含义是:

  • type:异常的类型。

  • value:异常的实例。

  • traceback:一个追溯对象,在最初发生异常的地方封装了调用堆栈。

如果没有发生异常,那么 exc_info 将是 None。

json(**kwargs)Link to this definition

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

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

如果 Content-Type 头不是 "application/json",那么在试图解析响应时将会出现一个 ValueError。

requestLink to this definition

激发响应的请求数据。

wsgi_requestLink to this definition

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

status_codeLink to this definition

整数形式的响应 HTTP 状态。关于定义代码的完整列表,查看 IANA status code registry.

templatesLink to this definition

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

resolver_matchLink to this definition

响应的 ResolverMatch 的实例。你可以使用 func 属性,例如,验证服务于响应的视图:

Code
# 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'] 来确定一个响应的内容类型。

例外Link to this heading

如果你把测试客户端指向一个会引发异常的视图,并且 Client.raise_request_exception 是 True,那么这个异常将在测试用例中可见。然后你可以使用标准的 try ... except 块或 assertRaises() 来测试异常。

测试客户端看不到的异常只有 Http404、PermissionDenied、SystemExit 和 SuspiciousOperation。Django 在内部捕获这些异常,并将其转换为相应的 HTTP 响应代码。在这些情况下,你可以在测试中检查 response.status_code。

如果 Client.raise_request_exception 为 False,测试客户端将返回一个 500 的响应,就像返回给浏览器一样。响应有属性 exc_info 来提供关于未处理的异常的信息。

持久状态Link to this heading

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

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

测试客户端具有存储持久状态信息的属性。你可以在测试条件的一部分访问这些属性。

Client.cookiesLink to this definition

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

Client.sessionLink to this definition

一个类似字典的对象,包含会话信息。详细内容请参见 会话文档。

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

Code
def test_something(self):
    session = self.client.session
    session["somekey"] = "test"
    session.save()
Client.asession()Link to this definition

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

设置语言Link to this heading

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

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

Code
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 头:

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

更多细节请参考 Django 如何发现语言偏好。

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

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

更多细节见 显式设置语言。

例如Link to this heading

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

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

提供的测试用例类Link to this heading

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

Django 单元测试类(TestCase 子类)的层次结构

Django 单元测试类的层次结构Link to this image

你可以将一个普通的 unittest.TestCase 转换为任何一个子类:将你的测试基类从 unittest.TestCase 改为子类。所有标准的 Python 单元测试功能都将是可用的,并且它将被一些有用的附加功能所增强,如下面每节所述。

SimpleTestCaseLink to this heading

class SimpleTestCaseLink to this definition

unittest.TestCase 的一个子类,增加了以下功能:

如果你的测试进行任何数据库查询,请使用子类 TransactionTestCase 或 TestCase。

SimpleTestCase.databasesLink to this definition

SimpleTestCase 默认不允许数据库查询。这有助于避免执行写查询而影响其他测试,因为每个 SimpleTestCase 测试不是在事务中运行的。如果你不关心这个问题,你可以通过在你的测试类上设置 databases 类属性为 '__all__' 来禁止这个行为。

TransactionTestCaseLink to this heading

class TransactionTestCaseLink to this definition

TransactionTestCase 继承自 SimpleTestCase 以增加一些数据库特有的功能:

Django 的 TestCase 类是 TransactionTestCase 的一个比较常用的子类,它利用数据库事务设施来加快在每次测试开始时将数据库重置到已知状态的过程。然而,这样做的一个后果是,有些数据库行为不能在 Django TestCase 类中进行测试。例如,你不能像使用 select_for_update() 时那样,测试一个代码块是否在一个事务中执行。在这些情况下,你应该使用 TransactionTestCase。

TransactionTestCase 和 TestCase 除了将数据库重设为已知状态的方式和测试与测试提交和回滚效果的相关代码外,其他都是相同的。

  • TransactionTestCase 在测试运行后,通过清空所有表来重置数据库。TransactionTestCase 可以调用提交和回滚,并观察这些调用对数据库的影响。

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

TestCaseLink to this heading

class TestCaseLink to this definition

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

此类:

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

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

它还提供了另一种方法:

classmethod TestCase.setUpTestData()Link to this definition

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

例如:

Code
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)Link to this definition

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

using 是数据库连接的别名,用于捕获回调。

如果 execute 是 True,并且如果没有发生异常,所有的回调将在上下文管理器退出时被调用。这模拟了在包裹的代码块之后的提交。

例如:

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

LiveServerTestCaseLink to this heading

class LiveServerTestCaseLink to this definition

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

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

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

Linux / macOS

Shell
$ python -m pip install "selenium >= 4.8.0"

Windows

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

然后,在你的应用程序的测试模块中添加一个基于 LiveServerTestCase 的测试(例如:myapp/tests.py)。在这个例子中,我们将假设你正在使用 staticfiles 应用,并且希望在执行测试时提供类似于我们在开发时使用 DEBUG=True 得到的静态文件,即不必使用 collectstatic 收集它们。我们将使用 StaticLiveServerTestCase 子类,它提供了这个功能。如果不需要的话,可以用 django.test.LiveServerTestCase 代替。

这个测试的代码可能如下:

Code
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()

最后,你可以按以下方式进行测试:

Linux / macOS

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

Windows

Windows
...\> manage.py test myapp.tests.MySeleniumTests.test_login

这个例子会自动打开 Firefox,然后进入登录页面,输入凭证并按“登录”按钮。Selenium 提供了其他驱动程序,以防你没有安装 Firefox 或希望使用其他浏览器。上面的例子只是 Selenium 客户端能做的一小部分,更多细节请查看 full reference。

测试用例特性Link to this heading

默认测试客户端Link to this heading

SimpleTestCase.clientLink to this definition

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

这意味着,不必每个测试中实例化一个 Client:

Code
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,像这样:

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

自定义测试客户端Link to this heading

SimpleTestCase.client_classLink to this definition

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

Code
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()

辅助工具加载Link to this heading

TransactionTestCase.fixturesLink to this definition

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

辅助工具是 Django 知道如何导入数据库的数据集合。例如,如果你的网站有用户账户,你可能会设置一个假用户账户的辅助工具,以便在测试时填充你的数据库。

创建辅助工具的最直接方法是使用 manage.py dumpdata 命令。这假定你已经在你的数据库中拥有一些数据。参见 dumpdata 文档 了解更多细节。

一旦你创建了一个辅助工具,并把它放在你的 INSTALLED_APPS 中的 fixtures 目录下,你就可以通过在你的 django.test.TestCase 子类上指定一个 fixtures 类属性来在你的单元测试中使用它。

Code
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 被调用后的状态。

  • 然后,所有命名的 fixtures 都会被安装。在这个例子中,Django 将首先安装名为 mammals 的任何 JSON fixture,然后是任何名为 birds 的 fixture。有关定义和安装 fixtures 的更多详细信息,请参阅 辅助工具 主题。

出于性能方面的考虑, TestCase 在 setUpTestData() 之前为整个测试类加载一次辅助工具,而不是在每次测试之前加载,并且它在每次测试之前使用事务来清理数据库。在任何情况下,你都可以确定一个测试的结果不会受到另一个测试或测试执行顺序的影响。

默认情况下,辅助工具只被加载到 default 数据库中。如果你使用多个数据库并且设置了 TransactionTestCase.databases,辅助工具将被加载到所有指定的数据库中。

URLconf 配置Link to this heading

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

多数据库支持Link to this heading

TransactionTestCase.databasesLink to this definition

Django 设置了一个测试数据库,对应于你设置中的 DATABASES 定义的并且至少有一个测试引用了 databases 的每个数据库。

然而,运行一个 Django TestCase 所花费的时间很大一部分是被调用 flush 所消耗的,它确保了你在每次测试运行开始时有一个干净的数据库。如果你有多个数据库,就需要多次刷新(每个数据库一个),这可能是一个耗时的活动——特别是当你的测试不需要测试多数据库活动时。

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

例如:

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

    def test_index_page_view(self):
        call_some_test_code()

这个测试用例类将在运行 test_index_page_view 之前刷新 default 和 other 测试数据库。你也可以使用 '__all__' 来指定所有的测试数据库都必须被刷新。

databases 标志也控制 TransactionTestCase.fixtures 被加载到哪些数据库。默认情况下,辅助工具只被加载到 default 数据库中。

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

TestCase.databasesLink to this definition

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

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

例如:

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

    def test_other_db_query(self): ...

这个测试只允许对 other 数据库进行查询。就像 SimpleTestCase.databases 和 TransactionTestCase.databases 一样,'__all__' 常量可以用来指定测试应该允许对所有数据库进行查询。

覆盖配置Link to this heading

SimpleTestCase.settings()Link to this definition

为了测试的目的,经常需要临时改变一个设置,并在运行测试代码后恢复到原始值。对于这个用例,Django 提供了一个标准的 Python 上下文管理器(见 PEP 343),叫做 settings(),可以这样使用:

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

这个示例将会在 with 块中覆盖 LOGIN_URL 设置的值,并在之后将其值重置为之前的状态。

SimpleTestCase.modify_settings()Link to this definition

重新定义包含一系列值的设置可能会很麻烦。在实践中,添加或删除值通常是足够的。Django 提供了 modify_settings() 上下文管理器,以方便更改设置:

Code
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("/")
            # ...

对于每个操作,你可以提供一个值的列表或一个字符串。当值已经存在于列表中时,append 和 prepend 没有效果;当值不存在时,remove 也没有效果。

override_settings(**kwargs)Link to this definition

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

Code
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 类:

Code
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)Link to this definition

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

Code
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("/")
        # ...

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

Code
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("/")
        # ...

你也可以在配置被覆盖后,通过删除配置来模拟没有配置,比如这样:

Code
@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

存储配置

隔离应用程序Link to this heading

utils.isolate_apps(*app_labels, attr_name=None, kwarg_name=None)Link to this definition

将包装上下文中定义的模型注册到它们自己隔离的 apps 注册表中。这个功能在为测试创建模型类时非常有用,因为这些类将在之后被干净地删除,不会出现名称冲突的风险。

应该将应该包含在隔离注册表中的应用程序标签作为单独的参数传递。你可以将 isolate_apps() 用作装饰器或上下文管理器。例如:

Code
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

        ...

… 或者:

Code
with isolate_apps("app_label"):

    class TestModel(models.Model):
        pass

    ...

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

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

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

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

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

Code
@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 参数将其作为测试方法的参数传递。

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

清空测试发件箱Link to this heading

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

关于测试期间电子邮件服务的更多细节,请参见下面的 Email services。

断言Link to this heading

由于 Python 的普通 unittest.TestCase 类实现了断言方法,比如 assertTrue() 和 assertEqual(),Django 的自定义 TestCase 类提供了一些有用于测试 Web 应用程序的自定义断言方法:

大多数这些断言方法给出的失败信息可以用 msg_prefix 参数自定义。这个字符串将被加在断言产生的任何失败信息的前面。这允许你提供额外的细节,以帮助你确定测试套件中失败的位置和原因。

SimpleTestCase.assertRaisesMessage(expected_exception, expected_message, callable, *args, **kwargs)Link to this definition
SimpleTestCase.assertRaisesMessage(expected_exception, expected_message)

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

如果只给了 expected_exception 和 expected_message 参数,则返回一个上下文管理器,以便被测试的代码可以内联而不是作为一个函数来写:

Code
with self.assertRaisesMessage(ValueError, "invalid literal for int()"):
    int("a")
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message, callable, *args, **kwargs)Link to this definition
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message)

类似于 SimpleTestCase.assertRaisesMessage(),但是 assertWarnsRegex() 代替 assertRaisesRegex()。

SimpleTestCase.assertFieldOutput(fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value='')Link to this definition

断言表单字段在不同的输入情况下表现正确。

参数:
  • fieldclass -- 待测试字段的类。

  • valid -- 一个字典,将有效输入映射到它们的预期干净值。

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

  • field_args -- 传递给实例化字段的 args。

  • field_kwargs -- 传递给实例化字段的 kwargs。

  • empty_value -- empty_values 中输入的预期干净输出。

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

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

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

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

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

errors 是字段预期具有的所有错误字符串的列表。如果只期望一个错误,也可以传递单个错误字符串,这意味着 errors='错误消息' 与 errors=['错误消息'] 是相同的。

SimpleTestCase.assertFormSetError(formset, form_index, field, errors, msg_prefix='')Link to this definition

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

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

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

field 和 errors 的含义与 assertFormError() 的参数相同。

SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False)Link to this definition

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

将 html 设置为 True,将 text 作为 HTML 处理。与响应内容的比较将基于 HTML 语义,而不是逐个字符的平等。在大多数情况下,空格会被忽略,属性排序并不重要。详见 assertHTMLEqual()。

SimpleTestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False)Link to this definition

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

将 html 设置为 True,将 text 作为 HTML 处理。与响应内容的比较将基于 HTML 语义,而不是逐个字符的平等。在大多数情况下,空格会被忽略,属性排序并不重要。详见 assertHTMLEqual()。

SimpleTestCase.assertTemplateUsed(response, template_name, msg_prefix='', count=None)Link to this definition

断言给定名称的模板被用于渲染响应。

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

template_name``应当是一个字符串,如'admin/index.html'``。

count 参数是一个整数,表示模板应该被渲染的次数。默认值是 None,表示模板应该被渲染一次或多次。

你可以把它作为一个上下文管理器,比如:

Code
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='')Link to this definition

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

你可以用 assertTemplateUsed() 一样的方式将其作为上下文管理器。

SimpleTestCase.assertURLEqual(url1, url2, msg_prefix='')Link to this definition

断言两个 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)Link to this definition

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

如果你的请求使用了 follow 参数,expected_url 和 target_status_code 将是重定向链最后一点的网址和状态码。

如果 fetch_redirect_response 为 False,则最终页面不会被加载。由于测试客户端不能获取外部 URL,所以如果 expected_url 不是 Django 应用的一部分,这一点就特别有用。

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

SimpleTestCase.assertHTMLEqual(html1, html2, msg=None)Link to this definition

断言字符串 html1 和 html2 相等。比较是基于 HTML 语义的。比较时考虑到以下因素:

  • HTML 标签前后的空白会被忽略。

  • 所有类型的空白都被认为是等价的。

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

  • 空标签相当于其自动闭合版。

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

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

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

下面的例子是有效的测试,并且没有引起任何 AssertionError:

Code
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>',
)

html1 和 html2 必须包含 HTML。如果其中一个不能被解析,将产生一个 AssertionError。

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

SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None)Link to this definition

断言字符串 html1 和 html2 不 相等。比较是基于 HTML 语义的。详见 assertHTMLEqual()。

html1 和 html2 必须包含 HTML。如果其中一个不能被解析,将产生一个 AssertionError。

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

SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None)Link to this definition

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

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

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

SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None)Link to this definition

断言字符串 xml1 和 xml2 不 相等。比较是基于 XML 语义的,参见 assertXMLEqual()。

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

SimpleTestCase.assertInHTML(needle, haystack, count=None, msg_prefix='')Link to this definition

断言 HTML 片段 needle 在 haystack 中出现一次。

如果指定了 count 整数参数,则将严格核查 needle 的出现次数。

在大多数情况下,空白是被忽略的,属性排序并不重要。参见 assertHTMLEqual() 以了解更多细节。

SimpleTestCase.assertNotInHTML(needle, haystack, msg_prefix='')Link to this definition

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

在大多数情况下,空白是被忽略的,属性排序并不重要。参见 assertHTMLEqual() 以了解更多细节。

SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None)Link to this definition

断言 JSON 片段 raw 和 expected_data 相等。通常的 JSON 非显性空格规则适用,因为重量级是委托给 json 库的。

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

SimpleTestCase.assertJSONNotEqual(raw, expected_data, msg=None)Link to this definition

断言 JSON 片段 raw 和 expected_data 不 相等。详见 assertJSONEqual()。

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

TransactionTestCase.assertQuerySetEqual(qs, values, transform=None, ordered=True, msg=None)Link to this definition

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

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

默认情况下,比较也是依赖于顺序的。如果 qs 不提供隐式排序,你可以将 ordered 参数设置为 False,这将使比较变成 collections.Counter 比较。如果顺序是未定义的(如果给定的 qs 不是有序的,并且比较的对象是一个以上的有序值),会产生一个 ValueError。

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

TransactionTestCase.assertNumQueries(num, func, *args, **kwargs)Link to this definition

断言当 func 与 *args 和 **kwargs 一起调用时,会执行 num 次数据库查询。

如果 kwargs 中存在 "using" 键,则使用该键作为数据库别名,以检查查询次数:

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

如果你想调用一个带有 using 参数的函数,你可以通过用 lambda 包装调用来增加一个额外的参数:

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

你也可以用它作为上下文管理器:

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

标记测试Link to this heading

你可以给你的测试打上标签,这样你就可以轻松地运行一个特定的子集。例如,你可以标记快速或慢速测试:

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

你还可以给一个测试用例类打上标签:

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

子类从超类继承标签,方法从其类继承标签。如:

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

SampleTestCaseChild.test 将用 'slow'、'core'、'bar' 和 'foo' 来标注。

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

Linux / macOS

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

Windows

Windows
...\> manage.py test --tag=fast

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

Linux / macOS

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

Windows

Windows
...\> manage.py test --tag=fast --tag=core

你也可以通过标签来排除测试。如果要运行不慢的核心测试:

Linux / macOS

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

Windows

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

test --exclud-tag 优先于 test --tag,所以如果一个测试有两个标签,你选择了其中一个而排除了另一个,测试就不会被运行。

测试异步代码Link to this heading

如果你只是想测试异步视图的输出,标准测试客户端将在自己的异步循环中运行它们,而不需要你做任何额外的工作。

但是,如果你想为 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)Link to this definition

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

  • 在初始化中,defaults 中的任意关键字参数都直接添加到 ASGI 作用域中。

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

    Python console
    >>> c = AsyncClient()
    >>> c.get("/customers/details/", {"name": "fred", "age": 7}, ACCEPT="application/json")
    

使用 AsyncClient 任何提出请求的方法都必须被等待:

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

异步客户端也可以调用同步视图;它通过 Django 的 异步请求路径 运行,它支持这两种方式。任何通过 AsyncClient 调用的视图都会得到一个 ASGIRequest 对象作为它的 request,而不是普通客户端创建的 WSGIRequest。

邮件服务Link to this heading

如果你的任何 Django 视图使用 Django 的邮件功能 发送电子邮件,你可能不想每次使用该视图运行测试时都发送电子邮件。出于这个原因,Django 的测试运行器会自动将所有 Django 发送的邮件重定向到一个虚拟的发件箱。这让你可以测试发送邮件的每一个方面——从发送邮件的数量到每封邮件的内容——而不用实际发送邮件。

测试运行器通过透明的将正常的邮件后端替换为测试后端来实现。(别担心——这对 Django 之外的其他邮件发送器没有影响,比如你机器的邮件服务器,如果你正在运行一个的话。)

django.core.mail.outboxLink to this definition

在测试运行过程中,每一封发出的邮件都会保存在 django.core.mail.outbox 中。这是所有已经发送的 EmailMessage 实例的列表。 outbox 属性是一个特殊的属性,只有在使用 locmem 邮件后端时才会创建。它通常不作为 django.core.mail 模块的一部分存在,你也不能直接导入它。下面的代码展示了如何正确访问这个属性。

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

Code
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:

Code
from django.core import mail

# Empty the test outbox
mail.outbox = []

管理命令Link to this heading

管理命令可以用 call_command() 函数来测试。输出可以重定向到 StringIO 实例中:

Code
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())

忽略测试Link to this heading

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

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

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

装饰器使用一个字符串标识符来描述数据库特征。这个字符串对应于数据库连接特征类的属性。参见 django.db.backends.base.features.BaseDatabaseFeatures 类 以获得可作为跳过测试基础的数据库特征的完整列表。

skipIfDBFeature(*feature_name_strings)Link to this definition

如果支持某个命名的数据库功能,则跳过装饰测试或 TestCase。

例如,如果数据库支持事务,下面的测试将不会被执行(例如,在PostgreSQL 下,它将 不 会运行,但在 MySQL 的 MyISAM 表下却可以):

Code
class MyTests(TestCase):
    @skipIfDBFeature("supports_transactions")
    def test_transaction_behavior(self):
        # ... conditional test code
        pass
skipUnlessDBFeature(*feature_name_strings)Link to this definition

如果 不 支持某个命名的数据库功能,则跳过装饰测试或 TestCase。

例如,接下来的测试仅在支持事务的数据库下执行(如:可以是PostgreSQL,但不可以是使用MyISAM数据库引擎的MySQL):

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