如何测试 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

INSTALLED_APPS = (
    ...
    'django_nose',
)
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
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 命令启动:

python manage.py testserver fixtures_dir/filename.yaml

在测试用例中指定:

from django.test import TestCase
from django.contrib.auth import authenticate

class LoginTest(TestCase):
    fixtures = ['mysite.yaml']

    def setUp(self):
        # 导入 fixture 中用户数据,省去创建用户的流程,也免去了清除用户数据的流程。

    def test_has_user(self):
        # 如果已导入 fixture 中数据,则可以使用其中的账号登录。
        self.assertIsNotNone(authenticate(username='windrunner', password='password'))

Client

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

import unittest
from django.test.client import Client

class PageTest(unittest.TestCase):
    def setUp(self):
        self.client = Client()

    def test_home(self):
        res = self.client.get('/')
        self.assertEqual(200, res.status_code)

    def test_login(self):
        """普通测试。client 实例会自动解决 csrf 问题。"""
        res = self.client.get('/login/')

        self.assertEqual(200, res.status_code)
        self.assertIn('Username', res.content)

        res_post = self.client.post('/login/', {'username': 'windrunner', 'password': 'password', })

        self.assertEqual(200, res_post.status_code)
        self.assertIn('windrunner', res_post.content)

    def test_login_csrf(self):
        """强制 csrf 检查"""
        self.client = Client(enforce_csrf_checks=True)          # 使用检查 CSRF 的 Client 示例代替默认实例
        res = self.client.get('/login/')
        csrf_token = '%s' % res.context['csrf_token']             # 获取 csrf_token

        res_fail = self.client.post('/login/', {'user': 'windrunner', 'pass': 'password', })
        self.assertEqual(403, res_fail.status_code)             # 没有处理 CSRF token 会返回 403 错误代码

        res_csrf = self.client.post('/login/', {'user': 'windrunner', 'pass': 'password', 'csrfmiddlewaretoken': csrf_token, })
        self.assertIn('windrunner', res_csrf.content)

    def test_logout(self):
        res = self.client.post('/logout/')
        self.assertEqual(302, res.status_code)

testserver

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

python manage.py testserver --addrport 7000 fixture1 fixture2

Selenium

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

import unittest
from selenium import webdriver
from django.contrib.auth import get_user_model, authenticate

class LoginTest(unittest.TestCase):
    def setUp(self):
        self.browser = webdriver.Firefox()          # 初始化浏览器,也可以选择 Chrome 或者 PhanatomJS

    def tearDown(self):
        self.browser.quit()                         # 测试结束后关闭浏览器

    def _login(self):
        # 这个方法没有以 ``test`` 开始,因此并不会单独被执行。
        self.browser.get('http://localhost:8000/login')         # 发送 GET 请求并打开页面

        # 使用浏览器的选择权选中 HTML 元素,并发送浏览器事件,复杂的元素选择可以借助 XPath
        self.browser.find_element_by_id('username').send_keys('windrunner')
        self.browser.find_element_by_id('password').send_keys('password')
        self.browser.find_element_by_id('submit').click()   # 触发点击事件

    def test_login(self):
        self._login()
        self.assertIn('windrunner', self.browser.page_source)   # 断言登录后的页面内容

    def test_logout(self):
        self._login()
        self.assertIn('windrunner', self.browser.page_source)
        self.browser.get('http://localhost:8000/logout')
        self.assertIn('nobody', self.browser.page_source)
        self.assertNotIn('windrunner', self.browser.page_source)