测试工具

测试工具

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

测试客户端

测试客户端是一个 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)[源代码]

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

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

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

query_params allows you to specify the default query string that will be set on every request.

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

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

备注

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

The values from the headers, query_params, and extra keyword arguments passed to get(), post(), etc. have precedence over the defaults passed to the class constructor.

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

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

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

Changed in Django 5.1:

The query_params argument was added.

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

  • get(path , data=None , follow=False , secure=False , , _headers=None , query_params=None , _*extra)[源代码]

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

The key-value pairs in the query_params dictionary are used to set query strings. For example:

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

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

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

It is also possible to pass these parameters into the data parameter. However, query_params is preferred as it works for any HTTP method.

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

>>> 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 变量。例如,设置脚本名称的头信息:

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

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

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

If you provide a URL with both an encoded GET data and either a query_params or data argument these arguments will take precedence.

如果将 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 请求。

Changed in Django 5.1:

The query_params argument was added.

  • post(path , data=None , content_type=MULTIPART_CONTENT , follow=False , secure=False , , _headers=None , query_params=None , _*extra)[源代码]

在提供的 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 (读取二进制)模式打开文件。

The headers, query_params, and extra parameters acts the same as for 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 请求。

Changed in Django 5.1:

The query_params argument was added.

  • head(path , data=None , follow=False , secure=False , , _headers=None , query_params=None , _*extra)[源代码]

Makes a HEAD request on the provided path and returns a Response object. This method works just like Client.get(), including the follow, secure, headers, query_params, and extra parameters, except it does not return a message body.

Changed in Django 5.1:

The query_params argument was added.

  • options(path , data=’’ , content_type=’application/octet-stream’ , follow=False , secure=False , , _headers=None , query_params=None , _*extra)[源代码]

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

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

The follow, secure, headers, query_params, and extra parameters act the same as for Client.get().

Changed in Django 5.1:

The query_params argument was added.

  • put(path , data=’’ , content_type=’application/octet-stream’ , follow=False , secure=False , , _headers=None , query_params=None , _*extra)[源代码]

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

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

The follow, secure, headers, query_params, and extra parameters act the same as for Client.get().

Changed in Django 5.1:

The query_params argument was added.

  • patch(path , data=’’ , content_type=’application/octet-stream’ , follow=False , secure=False , , _headers=None , query_params=None , _*extra)[源代码]

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

The follow, secure, headers, query_params, and extra parameters act the same as for Client.get().

Changed in Django 5.1:

The query_params argument was added.

  • delete(path , data=’’ , content_type=’application/octet-stream’ , follow=False , secure=False , , _headers=None , query_params=None , _*extra)[源代码]

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

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

The follow, secure, headers, query_params, and extra parameters act the same as for Client.get().

Changed in Django 5.1:

The query_params argument was added.

  • trace(path , follow=False , secure=False , , _headers=None , query_params=None , _*extra)[源代码]

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

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

The follow, secure, headers, query_params, and extra parameters act the same as for Client.get().

Changed in Django 5.1:

The query_params argument was added.

  • 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)还是使用测试夹具。记住,如果你想让你的测试用户有一个密码,你不能直接通过设置密码属性来设置用户的密码——你必须使用 set_password() 函数来存储一个正确的哈希密码。或者,你可以使用 create_user() 辅助方法来创建一个具有正确哈希密码的新用户。

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

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

Changed in Django 5.0:

aforce_login() 方法已添加。

  • logout()

  • alogout()

异步版本 :alogout()

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

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

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

New in 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-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)

参见

django.test.RequestFactory

提供的测试用例类

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

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

Django 单元测试类的层次结构

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

SimpleTestCase

class SimpleTestCase[源代码]

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

  • 一些有用的断言,例如:
    • 检查一个可调用对象 会引发某个异常。
    • 检查一个可调用对象 会触发某个警告。
    • 测试表单字段 渲染和错误处理。
    • 测试 HTML 响应是否存在/缺乏给定的片段。
    • 验证模板 是否被用于生成给定的响应内容。
    • 验证两个 URL 是否相等。
    • 验证应用程序是否执行了HTTP redirect。
    • 严格测试两个 HTML 片段 是否相等或 包含。
    • 严格测试两个 XML 片段 是否相等。
    • 严格测试两个 JSON 片段 相等。
  • 能够用 修改后的配置 运行测试。
  • 使用 client Client。

如果你的测试进行任何数据库查询,请使用子类 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[源代码]

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

  • 在每次测试开始时将数据库重新设置为已知状态,以方便测试和使用 ORM。
  • 数据库 fixtures.
  • 测试 基于数据库后端功能的跳过.
  • 剩下的专用 assert* 方法。

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

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

  • TransactionTestCase 在测试运行后,通过清空所有表来重置数据库。TransactionTestCase 可以调用提交和回滚,并观察这些调用对数据库的影响。
  • 另一方面,TestCase 在测试后不清空表。相反,它将测试代码包含在数据库事务中,在测试结束后回滚。这保证了测试结束时的回滚能将数据库恢复到初始状态。

警告

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

应用 不会看到他们的数据被重新加载;如果你需要这个功能(例如,第三方应用应该启用这个功能),你可以在 TestCase 中设置 serialized_rollback = True。

TestCase

class TestCase[源代码]

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

此类:

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

它还提供了另一种方法:

classmethod TestCase.setUpTestData()[源代码]

上文所述的类级 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)[源代码]

返回一个为给定的数据库连接捕获 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[源代码]

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

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

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

Linux/MacOS Windows

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

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

Linux/MacOS Windows

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

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() 中,但也可以使用 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()

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

  • 在每次测试开始时,在 setUp() 运行之前,Django 会对数据库进行刷新,将数据库直接返回到 migrate 被调用后的状态。
  • 然后,所有命名的 fixtures 都会被安装。在这个例子中,Django 将首先安装名为 mammals 的任何 JSON fixture,然后是任何名为 birds 的 fixture。有关定义和安装 fixtures 的更多详细信息,请参阅 辅助工具 主题。

出于性能方面的考虑, 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 之前刷新 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()[源代码]

为了测试的目的,经常需要临时改变一个设置,并在运行测试代码后恢复到原始值。对于这个用例,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/")

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

SimpleTestCase.modify_settings()[源代码]

重新定义包含一系列值的设置可能会很麻烦。在实践中,添加或删除值通常是足够的。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)[源代码]

如果你想覆盖一个测试方法的设置,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)[源代码]

同样,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 Default renderer
SERIALIZATION_MODULES 序列化器缓存
LOCALE_PATHS,LANGUAGE_CODE 默认翻译和加载的翻译
STATIC_ROOT, STATIC_URL, STORAGES 存储配置

Changed in Django 5.1:

Resetting the default renderer when the FORM_RENDERER setting is changed was added.

隔离应用程序

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。

(本文超出站点单文档体积上限,已按小节截断)