一般的 Python 单元测试类都会扩展一个基类 unittest.TestCase
。Django 提供了这个基类的一些扩展。
Django 单元测试类的层次结构
你可以将一个普通的 unittest.TestCase
转换为任何一个子类:将你的测试基类从 unittest.TestCase
改为子类。所有标准的 Python 单元测试功能都将是可用的,并且它将被一些有用的附加功能所增强,如下面每节所述。
unittest.TestCase 的一个子类,增加了以下功能:
一些有用的断言,例如:
如果你的测试进行任何数据库查询,请使用子类 TransactionTestCase
或 TestCase
。
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
继承自 SimpleTestCase
以增加一些数据库特有的功能:
fixtures
assert*
方法。Django 的 TestCase
类是 TransactionTestCase
的一个比较常用的子类,它利用数据库事务设施来加快在每次测试开始时将数据库重置到已知状态的过程。然而,这样做的一个后果是,有些数据库行为不能在 Django TestCase
类中进行测试。例如,你不能像使用 select_for_update()
时那样,测试一个代码块是否在一个事务中执行。在这些情况下,你应该使用 TransactionTestCase
。
TransactionTestCase
和 TestCase
除了将数据库重设为已知状态的方式和测试与测试提交和回滚效果的相关代码外,其他都是相同的。
TransactionTestCase
在测试运行后,通过清空所有表来重置数据库。TransactionTestCase
可以调用提交和回滚,并观察这些调用对数据库的影响。TestCase
在测试后不清空表。相反,它将测试代码包含在数据库事务中,在测试结束后回滚。这保证了测试结束时的回滚能将数据库恢复到初始状态。在不支持回滚的数据库上运行的 TestCase
(例如 MyISAM 存储引擎的 MySQL ),则 TransactionTestCase
的所有实例,将在测试结束时回滚,删除测试数据库中的所有数据。
应用 不会看到他们的数据被重新加载;如果你需要这个功能(例如,第三方应用应该启用这个功能),你可以在 TestCase
中设置 serialized_rollback = True
。
这是 Django 中最常用的编写测试的类。它继承自 TransactionTestCase
(以及扩展自 SimpleTestCase
)。如果你的 Django 应用程序不使用数据库,就使用 SimpleTestCase
。
atomic()
块中封装测试:一个用于整个类,一个用于每个测试。因此,如果你想测试一些特定的数据库事务行为,可以使用 TransactionTestCase
它还提供了另一种方法:
上文所述的类级 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()
将在每次测试前被调用,从而降低了速度优势。
返回一个为给定的数据库连接捕获 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
和 TransactionTestCase
的功能基本相同,但多了一个功能:它在设置时在后台启动一个实时的 Django 服务器,并在关闭时将其关闭。这就允许使用 Django 虚拟客户端 以外的自动化测试客户端,例如,Selenium 客户端,在浏览器内执行一系列功能测试,并模拟真实用户的操作。
实时服务器在 localhost 上监听,并绑定到 0 号端口,0 号端口使用操作系统分配的一个空闲端口。在测试过程中可以用 self.live_server_url
访问服务器的 URL。
为了演示如何使用 LiveServerTestCase
,让我们写一个 Selenium 测试。首先,你需要将 selenium package
安装到你的 Python 路径中。
...\> py -m pip install selenium
然后,在你的应用程序的测试模块中添加一个基于 LiveServerTestCase
的测试(例如:myapp/tests.py
)。在这个例子中,我们将假设你正在使用 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().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('%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 或希望使用其他浏览器。
当使用内存 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'))
这里的棘手之处在于,实际上并没有页面加载之类的东西,尤其是在服务器生成初始文档后动态生成 HTML 的现代 Web 应用程序中。 因此,检查响应中是否存在 <body>
可能不一定适用于所有用例。