测试工具

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

也可以选择社区维护的方案!

Django 拥有活跃的生态系统。“社区生态”页面列出了测试和测试数据方案,Django Packages 的“测试工具”分类还提供了更多选择。

测试客户端

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

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

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

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

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

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

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

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

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

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

概述和一个简单的例子

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

>>> 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 environ 变量。例如,要设置脚本名称:

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

说明

以 HTTP_ 前缀开头的关键字参数将被设置为头信息,但为了可读性,应该优先使用 headers 参数。

传递给 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)[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"},
... )

这会向 details 视图发送 HTTP_ACCEPT 请求头,是测试使用 django.http.HttpRequest.accepts() 方法的代码路径的一种好办法。

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

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

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

>>> 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/,你会看到以下内容:

>>> 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)[source]

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

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

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

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

/login/

…使用以下 POST 数据:

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 的字段提交三个选择值:

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

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

>>> 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 验证器。例如:

>>> 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 数据中可用。例如,如果你发出请求:

>>> 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)[source]

对提供的 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)[source]

在提供的 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)[source]

在提供的 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)[source]

在提供的 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)[source]

在提供的 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)[source]

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

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

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

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

异步版本:alogin()

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

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

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() 返回 True。

最后,在使用此方法之前,别忘了先创建用户账户。如前所述,测试运行器使用测试数据库,其中默认没有用户,因此生产站点中的有效账户不能直接用于测试。需要把创建用户纳入测试套件:可以使用 Django 模型 API 手动创建,也可以通过测试数据装载。若测试用户需要密码,不能直接设置 password 属性;必须调用 set_password() 存储正确散列后的密码。也可以使用 create_user() 辅助方法,在创建新用户时生成正确的密码散列。

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]。login() 调用的 authenticate() 函数通常会对用户进行注释。

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

logout()
alogout()

异步版本:alogout()

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

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

测试响应

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 status code registry.

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_exception 是 True,那么这个异常将在测试用例中可见。然后你可以使用标准的 try ... except 块或 assertRaises() 来测试异常。

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

如果 Client.raise_request_exception 为 False,测试客户端将返回一个 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()

这类似于 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-Language HTTP 头:

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 提供了这个基类的一些扩展。

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

Django 单元测试类的层次结构

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

SimpleTestCase

class SimpleTestCase[source]

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

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

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

如果在 setUpClass() 过程中出现异常,一定要考虑到 Python 的行为。如果发生这种情况,类中的测试和 tearDownClass() 都不会被运行。在 django.test.TestCase 的情况下,这将会泄露在 super() 中创建的事务,从而导致各种症状,包括在某些平台上的分段故障(在 macOS 上报告)。如果你想在 setUpClass() 中故意引发一个异常,如 unittest.SkipTest,一定要在调用 super() 之前进行,以避免这种情况。

TransactionTestCase

class TransactionTestCase[source]

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

Django 的 TestCase 是更常用的 TransactionTestCase 子类,它利用数据库事务,加快每个测试结束后将数据库恢复到已知状态的过程。但这也意味着,某些数据库行为无法在 Django 的 TestCase 中测试。例如,使用 select_for_update() 时要求代码块必须在事务中运行,而 TestCase 无法测试这种要求。在这些情况下,应使用 TransactionTestCase。

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

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

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

警告

在不支持回滚的数据库上运行的 TestCase (例如 MyISAM 存储引擎的 MySQL ),则 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 是数据库连接的别名,用于捕获回调。

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

例如:

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 号端口,0 号端口使用操作系统分配的一个空闲端口。在测试过程中可以用 self.live_server_url 访问服务器的 URL。

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

$ python -m pip install "selenium >= 4.23.0"
...\> py -m pip install "selenium >= 4.23.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,然后进入登录页面,输入凭证并按“登录”按钮。Selenium 提供了其他驱动程序,以防你没有安装 Firefox 或希望使用其他浏览器。上面的例子只是 Selenium 客户端能做的一小部分,更多细节请查看 full reference。

说明

当使用内存 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 FAQ 和 Selenium documentation 获取更多信息。

测试用例特性

默认测试客户端

SimpleTestCase.client
SimpleTestCase.async_client

每个 django.test.*TestCase 实例都可以通过 self.client 和 self.async_client 分别访问 Django 的同步和异步测试客户端。每次测试都会重新创建两个客户端,因此无需担心 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
SimpleTestCase.async_client_class

如果要使用其他 Client 或 AsyncClient 类,例如具有自定义行为的子类,可以设置 client_class 或 async_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()

异步测试客户端采用相同方式:继承 AsyncClient,再设置 async_client_class。

辅助工具加载

TransactionTestCase.fixtures

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

辅助工具是 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()

具体来说,将发生以下情况:

  • setUpClass() 期间会装载所有具名测试数据。在本例中,Django 先装载名为 mammals 的 JSON 测试数据,再装载名为 birds 的测试数据。定义和装载测试数据的更多细节见“辅助工具”主题。

对于使用 TestCase 的大多数单元测试,Django 不需要再执行其他操作,因为每个测试后通过事务清理数据库可提高性能。但对于 TransactionTestCase,会发生以下操作:

  • 每个测试结束时,Django 都会清空数据库,将它恢复到刚调用 migrate 后的状态。

  • 在后续每个测试的 setUp() 执行之前,重新装载测试数据。

无论哪种情况,都可以确信某个测试的结果不会受到其他测试或测试执行顺序的影响。

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

URLconf 配置

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

多数据库支持

TransactionTestCase.databases

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

不过,运行 Django TestCase 的大量时间用于调用 flush,以保证每轮测试结束后数据库保持干净。如果存在多个数据库,每个数据库都要执行一次 flush,这可能很耗时,尤其是测试本身并不需要验证多数据库行为时。

作为优化,Django 默认只会在每轮测试结束时清空 default 数据库。如果配置了多个数据库,而某个测试需要所有数据库都保持干净,可以通过测试套件的 databases 属性请求清空其他数据库。

例如:

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

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

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

例如:

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

    def test_other_db_query(self): ...

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

覆盖配置

警告

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

SimpleTestCase.settings()[source]

测试时经常需要临时修改一个设置,并在测试代码执行后恢复原值。Django 为此提供了标准上下文管理器 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/")

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

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

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

override_settings(**kwargs)[source]

如果要针对某个测试方法覆盖设置,Django 提供了 override_settings() 装饰器:

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

说明

当给定一个类时,这些装饰器直接修改该类并返回它,它们不会创建并返回一个修改后的副本。因此,如果你试图调整上面的例子,将返回值分配给一个不同于 LoginTestCase 或 MiddlewareTestCase 的名称,你可能会惊讶地发现,原来的测试用例类仍然同样受到装饰器的影响。对于一个给定的类,modify_settings() 总是应用在 override_settings() 之后。

警告

配置文件中包含了一些设置,这些设置只有在 Django 内部初始化时才会被使用。如果你用 override_settings 改变它们,当你通过“django.conf.settings“ 模块访问会得到被改变的配置。但是,Django 的内部程序访问它的方式是不同的。实际上,使用 override_settings() 或者 modify_settings() 来使用这些设置,很可能达不到你预期的效果。

我们不建议改变 DATABASES 的设置。改变 CACHES 的设置是可能的,但如果你使用的是内部缓存,比如 django.contrib.session,就有点棘手。例如,你必须在使用缓存会话并覆盖 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

存储配置

隔离应用程序

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:如果用作函数装饰器,传递隔离注册表的关键字参数。

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

@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 类,测试运行器将在每个测试用例开始时清除测试邮件发件箱的内容。

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

断言

由于 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_exception 和 expected_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 — 传递给实例化字段的 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(form, field, errors, msg_prefix=”)[source]

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

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

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

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

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

断言 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)[source]

断言 HttpResponse 具有给定的 status_code,并且内容中出现了 text。如果提供 count,则要求 text 在响应中恰好出现 count 次。

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

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

断言 HttpResponse 具有给定的 status_code,并且内容中没有出现 text。

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

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

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

response 必须是测试客户端返回的 Response 实例。

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]

断言 HttpResponse 返回重定向状态,其判断基于 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)[source]

断言字符串 html1 和 html2 相等。比较是基于 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>',
)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

SimpleTestCase.assertNotInHTML(needle, haystack, msg_prefix=”)[source]

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

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

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

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

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

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

断言 JSON 片段 raw 和 expected_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]

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

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

self.assertNumQueries(7, my_function, 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 --exclud-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")
    

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

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。

警告

如果你使用的是测试装饰器,它们必须是异步兼容的,以确保它们正确工作。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 发送的邮件重定向到内存发件箱。这样即可测试邮件发送的各个方面,包括发送数量和每封邮件的内容,而无需实际发送。

测试运行器会将每个已配置 MAILERS 别名的 BACKEND 透明地替换为 ‘locmem’ 内存邮件后端。无需担心:这不会影响 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"],
        )

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

        # Verify the message was sent with the "default" mailer.
        self.assertEqual(mail.outbox[0].sent_using, "default")
Changed in Django 6.1:

发件箱中的邮件新增了 sent_using 属性。

正如 之前,在 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 下,它将 不 会运行,但在 MySQL 的 MyISAM 表下却可以):

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

原文:测试工具。作者/来源:Django 项目与文档贡献者。本文依据所列原文整理为中文,代码、命令与配置示例保留原文。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容