测试工具

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

测试客户端

测试客户端是一个Python类,作为一个虚拟的Web浏览器,允许您测试您的视图,并与您的Django供电的应用程序以编程方式交互。

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

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

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

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

一个全面的测试套件应该使用两种测试类型的组合。

概述和一个快速例子

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

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

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

请注意测试客户端如何工作的几个重要的事情:

  • 测试客户端要求Web服务器正在运行。 事实上,它将运行很好,没有Web服务器运行在所有! 这是因为它避免了HTTP的开销,直接处理Django框架。 这有助于使单元测试快速运行。

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

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

    这是不正确的:

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

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

  • 要解析网址,测试客户端将使用您的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类发出请求。

客户端enforce_csrf_checks = False** defaults[source]

它在建设时不需要论证。 但是,您可以使用关键字参数指定一些默认标头。 例如,这会在每个请求中发送User-Agent HTTP标头:

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

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

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

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

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

在提供的Response上发出GET请求,并返回path对象,下面将对此进行说明。

data字典中的键值对用于创建GET数据有效内容。 像这样:

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

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

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

extra关键字arguments参数可用于指定要在请求中发送的标头。 像这样:

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

...将发送HTTP头HTTP_X_REQUESTED_WITH到详细信息视图,这是测试使用django.http.HttpRequest.is_ajax()方法的代码路径的好方法。

CGI规范

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

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

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

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

如果您将follow设置为True,则客户端将跟踪任何重定向,并且将在包含中间网址元组的响应对象中设置redirect_chain属性和状态代码。

如果您有重定向到/next/的网址/redirect_me/,则重定向到/final/,这是您会看到的:

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

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

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

在提供的Response上发出POST请求,并返回path对象,这在下面进行了说明。

data字典中的键值对用于提交POST数据。 像这样:

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

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

/login/

...使用此POST数据:

name=fred&passwd=secret

如果您为XML有效内容提供data(例如text / xml),则content_type的内容将作为POST请求,使用HTTP content_type标头中的Content-Type

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

要为给定键提交多个值 - 例如,为&lt; select multiple>指定选择,列表或元组。 例如,choices的此值将为名为data的字段提交三个选定值:

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

提交文件是一种特殊情况。 要发布文件,您只需要提供文件字段名作为键,以及要作为值上传的文件的文件句柄。 像这样:

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

(名称attachment这里不相关;使用您的文件处理代码所期望的任何名称。)

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

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

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

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

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

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

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

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

如果您将True设置为follow,则客户端将跟踪任何重定向,并且将在包含中间网址元组的响应对象中设置redirect_chain属性和状态代码。

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

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

在提供的Response上执行HEAD请求,并返回path对象。 此方法的工作方式与Client.get()一样,除了它之外,包括followsecureextra

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

在提供的Response上执行TRACE请求,并返回path对象。 用于模拟诊断探头。

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

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

login(**credentials)[source]

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

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

credentials参数的格式取决于您使用的authentication backend(由您的AUTHENTICATION_BACKENDS设置配置)。 如果您使用的是由Django提供的标准认证后端(credentials),则ModelBackend应该是用户的用户名和密码,

>>> 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()助手方法创建具有正确散列密码的新用户。

在Django更改1.10:

在以前的版本中,不允许非活动用户登录(is_active=False)。

force_login(user, backend=None)[source]

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

login()不同,该方法跳过验证和验证步骤:不允许用户(is_active=False)登录,不需要提供用户凭据。

用户将其backend属性设置为backend参数(它应该是一个虚线的Python路径字符串)或settings.AUTHENTICATION_BACKENDS[0]如果没有提供值。 通过login()调用的authenticate()函数通常会像这样注释用户。

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

logout()[source]

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

调用此方法后,测试客户端将所有Cookie和会话数据清除为默认值。 后续请求将显示为来自AnonymousUser

测试响应

post()get()方法都会返回Response对象。 This Response object is not the same as the HttpResponse object returned by Django views; the test response object has some additional data useful for test code to verify.

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

class Response
client

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

content

身体的反应,作为一个毕业生。 这是视图呈现的最终页面内容,或任何错误消息。

context

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

如果呈现的页面使用多个模板,则Context将是context对象的列表,按照它们的呈现顺序。

无论渲染期间使用的模板数量如何,都可以使用[]运算符检索上下文值。 例如,可以使用以下方式检索上下文变量name

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

不使用Django模板?

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

json(**kwargs)

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

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

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

request

刺激响应的请求数据。

wsgi_request

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

status_code

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

templates

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

不使用Django模板?

仅当使用DjangoTemplates后端时,才会填充此属性。 如果您正在使用另一个模板引擎,那么如果您只需要用于呈现的模板名称,那么template_name可能是一个合适的替代方法。

resolver_match

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

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

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

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

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

例外¶ T0>

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

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

持久状态

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

不遵循这些cookie的过期政策。 如果您希望Cookie过期,请手动删除或创建新的Client实例(这将有效删除所有Cookie)。

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

Client.cookies

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

Client.session

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

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

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

设定语言

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

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

from django.conf import settings

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

或者通过在请求中包含Accept-Language HTTP头;

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

更多细节在How Django discovers language preference

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

from django.utils import translation

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

更多详细信息,请参见Explicitly setting the active language

实施例¶ T0>

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

import unittest
from django.test import Client

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

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

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

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

提供测试用例类

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

Hierarchy of Django unit testing classes (TestCase subclasses)

Django单元测试类的层次结构

将正常的unittest.TestCase转换为任何子类很容易:将测试的基类从unittest.TestCase更改为子类。 所有标准的Python单元测试功能将可用,并且将会增加一些有用的添加,如下面的每个部分所述。

SimpleTestCase

class SimpleTestCase[source]

添加此功能的unittest.TestCase的子类:

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

SimpleTestCase。 allow_database_queries T0> ¶ T1>

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

警告

SimpleTestCase及其子类(例如TestCase,...)依赖于setUpClass()tearDownClass()执行一些全班初始化(例如覆盖设置)。 如果需要重写这些方法,不要忘记调用super实现:

class MyTestCase(TestCase):

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

    @classmethod
    def tearDownClass(cls):
        ...
        super(MyTestCase, cls).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()时所需。 在这些情况下,您应该使用TransactionTestCase

TestCaseTransactionTestCase是完全相同的,除了数据库重置为已知状态的方式以及测试代码测试提交和回滚效果的能力:

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

警告

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

Apps will not see their data reloaded;如果您需要此功能(例如,第三方应用程序应启用此功能),您可以设置serialized_rollback = True t2 > TestCase正文内。

TestCase

class TestCase[source]

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

班上:

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

在每个测试结束时添加了对可延迟数据库约束的检查。

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

classmethod TestCase.setUpTestData()[source]

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

像这样:

from django.test import 测试用例

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()中创建的任何对象。 在类级别完成的设置工作中对内存中对象的修改将在测试方法之间持续存在。 如果确实需要修改它们,可以使用例如refresh_from_db()setUp()方法中重新加载它们。

LiveServerTestCase

class LiveServerTestCase[source]

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

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

在Django更改1.11:

在旧版本中,Django尝试了一个预定义的端口范围,可以以各种方式进行定制,包括DJANGO_LIVE_TEST_SERVER_ADDRESS环境变量。 这被删除,有利于更简单的“绑定到端口0”技术。

为了演示如何使用LiveServerTestCase,我们来写一个简单的Selenium测试。 首先呢,你需要用pip命令安装 selenium package 到你的Python路径里面:

$ pip install selenium

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

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

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

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

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

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

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

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

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

此示例将自动打开Firefox,然后转到登录页面,输入凭据并按“登录”按钮。 Selenium提供其他驱动程序,以防您没有安装Firefox或希望使用其他浏览器。 上面的例子只是Selenium客户端可以做的一小部分;有关详细信息,请查看完整参考

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

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

这里的棘手的事情是,真的没有一个“页面加载”,尤其是在现代Web应用程序中,在服务器生成初始文档后动态生成HTML。 因此,简单地检查响应中是否存在<body>可能不一定适用于所有用例。 有关详细信息,请参阅Selenium常见问题Selenium文档

测试用例特征

默认测试客户端

SimpleTestCase.client

每个测试用例在a django.test。*测试用例 实例可以访问Django测试客户端的一个实例。 此客户端可以作为self.client访问。 每个测试都重新创建此客户端,因此您不必担心从一个测试到另一个测试的状态(例如Cookie)。

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

import unittest
from django.test import Client

class SimpleTest(unittest.测试用例):
    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 测试用例

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 TestCase, Client

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

class MyTest(TestCase):
    client_class = MyTestClient

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

夹具加载

TransactionTestCase.fixtures

如果数据库中没有任何数据,则数据库支持的网站的测试用例不会太多。 测试更可读,并且使用ORM创建对象更为可维护,例如在TestCase.setUpTestData()中,但是您也可以使用fixture。

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

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

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

from django.test import 测试用例
from myapp.models import Animal

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

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

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

这里具体是什么会发生:

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

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

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

URLconf配置

如果您的应用程序提供了视图,您可能需要包括使用测试客户端来执行这些视图的测试。 但是,最终用户可以在自己选择的任何URL上自由地在应用程序中部署视图。 使用@override_settings(ROOT_URLCONF=...)装饰您的测试类或测试方法以进行URLconf配置。

多数据库支持

TransactionTestCase.multi_db

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

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

像这样:

class TestMyViews(TestCase):
    multi_db = True

    def test_index_page_view(self):
        call_some_test_code()

此测试用例将在运行test_index_page_view之前冲洗所有测试数据库。

multi_db标志也会影响加载了TransactionTestCase.fixtures的数据库。 默认情况下(当default时),fixture仅加载到multi_db=False数据库中。 如果multi_db=True,fixture将加载到所有数据库中。

覆盖设置

警告

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

SimpleTestCase.settings()[source]

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

from django.test import 测试用例

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]

它可以证明难以重新定义包含值列表的设置。 在实践中,添加或删除值通常就足够了。 modify_settings()上下文管理器使其变得容易:

from django.test import 测试用例

class MiddlewareTestCase(TestCase):

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

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

override_settings()[source]

如果要覆盖测试方法的设置,Django提供override_settings()装饰器(请参阅 PEP 318)。 它的使用像这样:

from django.test import TestCase, override_settings

class LoginTestCase(TestCase):

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

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

from django.test import TestCase, override_settings

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

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

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

from django.test import TestCase, modify_settings

class MiddlewareTestCase(TestCase):

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

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

from django.test import TestCase, modify_settings

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

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

当给一个类时,这些装饰器直接修改类并返回它;它们不会创建并返回其修改的副本。 因此,如果您尝试调整上述示例以将返回值分配给不同于MiddlewareTestCaseLoginTestCase的名称,您可能会惊讶地发现原始测试用例类仍然同样受到装饰师的影响。 对于给定类,modify_settings()始终应用于override_settings()之后。

警告

设置文件包含一些仅在Django内部初始化期间参考的设置。 如果使用django.conf.settings更改这些设置,如果通过override_settings模块访问它,设置会更改,但是Django的内部访问方式不同。 有效地,使用这些设置使用override_settings()modify_settings()可能不会做你期望做的。

我们不建议更改DATABASES设置。 改变CACHES设置是可能的,但如果你使用使用缓存的内部,如django.contrib.sessions,有点棘手。 例如,您必须在使用缓存会话并覆盖CACHES的测试中重新初始化会话后端。

最后,避免将您的设置作为模块级常量别名,因为override_settings()将不适用于这些值,因为它们仅在首次导入模块时进行评估。

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

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

覆盖设置时,请确保处理您的应用代码使用缓存或类似功能的情况,即使设置更改也保留状态。 Django提供了django.test.signals.setting_changed信号,允许您注册回调以清除设置,否则在更改设置时重置状态。

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

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

排除测试发件箱

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

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

断言¶ T0>

由于Python的普通unittest.TestCase类实现了断言方法,例如assertTrue()assertEqual(),Django的定制TestCase

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

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

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

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

with self.assertRaisesMessage(ValueError, 'invalid literal for int()'):
    int('a')

自1.9版以来已弃用 作为callable_obj的关键字参数传递callable已被弃用。 将可呼叫作为位置参数传递。

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

断言表单字段在各种输入中正确运行。

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

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

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

断言窗体上的字段在表单上呈现时会提供所提供的错误列表。

Form是在模板上下文中给出的form实例的名称。

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

errors是预期为表单验证结果的错误字符串或错误字符串列表。

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

断言formset在呈现时引发提供的错误列表。

Formset是在模板上下文中给出的formset实例的名称。

Formsetform_index内的表单编号。 如果None的值为form_index,则将检查非格式错误(可通过formset.non_form_errors()访问的错误)。

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

errors是预期为表单验证结果的错误字符串或错误字符串列表。

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

断言status_code实例生成给定的Response,并且该text显示在响应的内容中。 如果提供了text,则count必须在响应中出现正好count次数。

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

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

认为status_code实例产生给定的Response,并且text不会不会出现在响应的内容中。

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

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

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

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

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

你可以使用它作为上下文管理器,像这样:

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

声明使用给定名称的模板来呈现响应。

您可以使用与assertTemplateUsed()相同的方式作为上下文管理器。

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

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

如果您的请求使用expected_url参数,则followtarget_status_code将是重定向链最后一点的网址和状态代码。

如果Falsefetch_redirect_response,则不会加载最后一页。 由于测试客户端无法获取外部URL,所以如果expected_url不是Django应用程序的一部分,那么这一点尤其有用。

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

自1.9版以来已弃用 host参数已被弃用,因为重定向不再被强制为绝对URL。

SimpleTestCase。assertHTMLEqualhtml1html2msg =无[source] ¶ T6>

断言字符串html2html1是相等的。 比较基于HTML语义。 比较需要考虑以下因素:

  • 忽略HTML标记之前和之后的空格。
  • 所有类型的空格都被视为等同。
  • 所有打开的标签被隐式地关闭,例如。当周围标签关闭或HTML文档结束时。
  • 空标记等同于其自我关闭版本。
  • HTML元素的属性顺序并不重要。
  • 没有参数的属性等于名称和值相等的属性(参见示例)。

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

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

html2html1必须是有效的HTML。 如果其中一个无法解析,则会引发AssertionError

出错时的输出可以用msg参数定制。

SimpleTestCase。assertHTMLNotEqualhtml1html2msg =无[source] ¶ T6>

认为字符串html2html1不是相等。 比较基于HTML语义。 有关详细信息,请参见assertHTMLEqual()

html2html1必须是有效的HTML。 如果其中一个无法解析,则会引发AssertionError

出错时的输出可以用msg参数定制。

SimpleTestCase。assertXMLEqualxml1xml2msg =无[source] ¶ T6>

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

出错时的输出可以用msg参数定制。

SimpleTestCase。assertXMLNotEqualxml1xml2msg =无[source] ¶ T6>

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

出错时的输出可以用msg参数定制。

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

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

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

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

SimpleTestCase。assertJSONEqualrawexpected_datamsg =无[source] ¶ T6>

断言JSON片段expected_dataraw是相等的。 通常的JSON非重要空格规则适用于将重量级委派给json库。

出错时的输出可以用msg参数定制。

SimpleTestCase。assertJSONNotEqualrawexpected_datamsg =无[source] ¶ T6>

断言JSON片段expected_dataraw不是相等。 有关详细信息,请参见assertJSONEqual()

出错时的输出可以用msg参数定制。

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

断言查询集values返回值qs

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

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

出错时的输出可以用msg参数定制。

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

断言当使用*argsfunc调用**kwargs时,将执行num数据库查询。

如果kwargs中存在"using"键,它将用作要检查查询数的数据库别名。 如果你想用lambda参数调用一个函数,你可以通过用using包装调用来添加一个额外的参数:

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

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

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

标签测试

Django中的新功能1.10。

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

from django.test import tag

class SampleTestCase(TestCase):

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

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

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

您还可以标记测试用例:

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

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

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

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

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

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

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

test --exclude-tag优先于test --tag,所以如果一个测试有两个标签,你选择其中一个标签并排除其他标签,测试将不会运行。

电子邮件服务

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

测试运行器通过用测试后端透明地替换普通电子邮件后端来实现这一点。 (不要担心,这对Django之外的任何其他电子邮件发件人(例如您计算机的邮件服务器)(如果您正在运行)没有任何影响。)

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

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

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

from django.core import mail
from django.test import 测试用例

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

如前所述previously,测试发件箱在Django *TestCase中的每个测试开始时都被清空。 要手动清空发件箱,请将空列表分配给mail.outbox

from django.core import mail

# Empty the test outbox
mail.outbox = []

管理命令

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

from django.core.management import call_command
from django.test import 测试用例
from django.utils.six import StringIO

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

跳过测试

unittest库提供@skipIf@skipUnless装饰器,如果提前知道这些测试在某些条件下会失败,您可以跳过测试。

例如,如果您的测试需要特定的可选库来成功,您可以使用@skipIf来装饰测试用例。 然后,测试运行器将报告测试没有被执行以及为什么,而不是失败测试或完全省略测试。

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

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

skipIfDBFeature(*feature_name_strings)[source]

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

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

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

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

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

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