如何测试 Django 应用?

Django 的启动互相之间的依赖严重,很多参数和依赖都需要在运行的时候导入,导致大部分文件都不能单独执行。
不过 Django 的社区非常活跃,对于知名的测试框架都有进行封装,如: django.testdjango_nose 等等,
以配合自身的测试命令使用。

doctest

在 Flask 中测试一个文件的 doctest 只需要运行:python filename.py,然而这在 Django 中行不通。
在 Django 中依赖自身的 test 命令:python manage.py test[ app_name],其中 app_name 若为空
默认测试所有应用。在 1.6 及以后版本中
需要首先在 settings.py 中指定 TEST_RUNNER

  1. INSTALLED_APPS = (
  2. ...
  3. 'django_nose',
  4. )
  5. TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
  6. NOSE_ARGS = ['--with-doctest']

TestCase

Django 的 TestCase 类是 unittest.TestCase 的子类,使用起来非常相似。

Fixture

Fixture 是 unittest 提供读取测试数据的一种方式,在 Django 的 TestCase 中也可以直接使用,使用前需要导出数据:

python manage.py dumpdata --format=yaml --indent=4 > fixtures_dir/filename.yaml

支持的数据格式包括 YAML、JSON 等等,YAML 可读性较高,不过需要安装额外的依赖。

配合 testserver 命令启动:

  1. python manage.py testserver fixtures_dir/filename.yaml

在测试用例中指定:

  1. from django.test import TestCase
  2. from django.contrib.auth import authenticate
  3. class LoginTest(TestCase):
  4. fixtures = ['mysite.yaml']
  5. def setUp(self):
  6. # 导入 fixture 中用户数据,省去创建用户的流程,也免去了清除用户数据的流程。
  7. def test_has_user(self):
  8. # 如果已导入 fixture 中数据,则可以使用其中的账号登录。
  9. self.assertIsNotNone(authenticate(username='windrunner', password='password'))

Client

Client 提供了用户代理的模拟,其使用类似于 requests 库,不过使用前需要先初始化:client = Client()
Client 默认会提供 CSRF 认证,如果需要手动验证 CSRF,需要这样初始化:
csrf_enabled_client = Client(enforce_csrf_checks=True)

  1. import unittest
  2. from django.test.client import Client
  3. class PageTest(unittest.TestCase):
  4. def setUp(self):
  5. self.client = Client()
  6. def test_home(self):
  7. res = self.client.get('/')
  8. self.assertEqual(200, res.status_code)
  9. def test_login(self):
  10. """普通测试。client 实例会自动解决 csrf 问题。"""
  11. res = self.client.get('/login/')
  12. self.assertEqual(200, res.status_code)
  13. self.assertIn('Username', res.content)
  14. res_post = self.client.post('/login/', {'username': 'windrunner', 'password': 'password', })
  15. self.assertEqual(200, res_post.status_code)
  16. self.assertIn('windrunner', res_post.content)
  17. def test_login_csrf(self):
  18. """强制 csrf 检查"""
  19. self.client = Client(enforce_csrf_checks=True) # 使用检查 CSRF 的 Client 示例代替默认实例
  20. res = self.client.get('/login/')
  21. csrf_token = '%s' % res.context['csrf_token'] # 获取 csrf_token
  22. res_fail = self.client.post('/login/', {'user': 'windrunner', 'pass': 'password', })
  23. self.assertEqual(403, res_fail.status_code) # 没有处理 CSRF token 会返回 403 错误代码
  24. res_csrf = self.client.post('/login/', {'user': 'windrunner', 'pass': 'password', 'csrfmiddlewaretoken': csrf_token, })
  25. self.assertIn('windrunner', res_csrf.content)
  26. def test_logout(self):
  27. res = self.client.post('/logout/')
  28. self.assertEqual(302, res.status_code)

testserver

testserver 是 Django 提供的启动测试服务器的方法,会创建一个测试数据库来替代默认数据库,
通常会在启动时导入相应 fixture。命令如下:

  1. python manage.py testserver --addrport 7000 fixture1 fixture2

Selenium

因为 Selenium 是控制浏览器测试 web 服务,因此并不会受到 Django 的干扰,这里有一段示例代码:

  1. import unittest
  2. from selenium import webdriver
  3. from django.contrib.auth import get_user_model, authenticate
  4. class LoginTest(unittest.TestCase):
  5. def setUp(self):
  6. self.browser = webdriver.Firefox() # 初始化浏览器,也可以选择 Chrome 或者 PhanatomJS
  7. def tearDown(self):
  8. self.browser.quit() # 测试结束后关闭浏览器
  9. def _login(self):
  10. # 这个方法没有以 ``test`` 开始,因此并不会单独被执行。
  11. self.browser.get('http://localhost:8000/login') # 发送 GET 请求并打开页面
  12. # 使用浏览器的选择权选中 HTML 元素,并发送浏览器事件,复杂的元素选择可以借助 XPath
  13. self.browser.find_element_by_id('username').send_keys('windrunner')
  14. self.browser.find_element_by_id('password').send_keys('password')
  15. self.browser.find_element_by_id('submit').click() # 触发点击事件
  16. def test_login(self):
  17. self._login()
  18. self.assertIn('windrunner', self.browser.page_source) # 断言登录后的页面内容
  19. def test_logout(self):
  20. self._login()
  21. self.assertIn('windrunner', self.browser.page_source)
  22. self.browser.get('http://localhost:8000/logout')
  23. self.assertIn('nobody', self.browser.page_source)
  24. self.assertNotIn('windrunner', self.browser.page_source)