如何为Django视图编写单元测试?


68

我在理解应该如何为Django设计单元测试方面遇到问题。

从我的理解来看,一次性测试整个视图似乎是不可能的。我们需要区分请求的发布前和发布后状态。但是我不知道该如何设计。有没有现实生活中的例子?

查看文档,这些示例过于简化,仅关注模型。

@login_required
def call_view(request, contact_id):
    profile = request.user.get_profile()
    if request.POST:        
        form = CallsForm(profile.company, request.POST)           
        if form.is_valid()
        return HttpResponseRedirect('/contact/' + contact_id + '/calls/')
    else:        
        form = CallsForm(profile.company, instance=call)              
    variables = RequestContext(request, {'form':form}
    return render_to_response('conversation.html', variables)

更新:

尝试使成功的测试工作成功,但仍然失败:

def test_contact_view_success(self):
    # same again, but with valid data, then
    self.client.login(username='username1', password='password1')
    response = self.client.post('/contact/add/', {u'last_name': [u'Johnson'], }) 
    self.assertRedirects(response, '/')

错误信息:

AssertionError: Response didn't redirect as expected: Response code was 200 (expected 302)

我认为这是因为form.is_valid()失败并且它不会重定向,对吗?


6
亲提示:使用render()快捷方式,而不是手动创建的RequestContext并调用render_to_response()每个时间; return render(request, 'conversation.html', variables)
supervacuo 2012年

4
此外,Django文档建议反对if request.POST,并建议if request.method == 'POST'改为(有时,一个POST请求将有一个空的request.POST,哪些测试为假,并触发了错误的分支)。
supervacuo 2012年

谢谢您的好提示。高度赞赏:)
侯曼(Houman)2012年

Answers:


108

NB NB! 严格来说,这不是“单元测试”;为Django视图代码编写独立的单元测试既困难又罕见。这更多是一个集成测试...

您说对了,有几种途径是对的:

  1. GETPOST由匿名用户(应重定向到登录页面)
  2. GETPOST没有配置文件的登录用户(应引发UserProfile.DoesNotExist异常)
  3. GET 由登录用户(应显示表格)
  4. POST 由具有空白数据的登录用户(应显示表单错误)
  5. POST 由登录用户使用无效数据(应显示表单错误)
  6. POST 由具有有效数据的登录用户(应重定向)

测试1实际上只是测试@login_required,因此您可以跳过它。无论如何,我倾向于进行测试(以防万一我们忘记使用该装饰器)。

我不知道在故障情况下(500错误页面)2是你真正想要的。我可以算出您想发生的事情(也许使用get_or_create(),或者捕获DoesNotExist异常并以这种方式创建新的配置文件),或者重定向到页面以供用户创建配置文件。

根据你有多少自定义验证有,4未必真的需要进行测试。

无论如何,鉴于以上所有情况,我将执行以下操作:

from django.test import TestCase

class TestCalls(TestCase):
    def test_call_view_deny_anonymous(self):
        response = self.client.get('/url/to/view', follow=True)
        self.assertRedirects(response, '/login/')
        response = self.client.post('/url/to/view', follow=True)
        self.assertRedirects(response, '/login/')

    def test_call_view_load(self):
        self.client.login(username='user', password='test')  # defined in fixture or with factory in setUp()
        response = self.client.get('/url/to/view')
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'conversation.html')

    def test_call_view_fail_blank(self):
        self.client.login(username='user', password='test')
        response = self.client.post('/url/to/view', {}) # blank data dictionary
        self.assertFormError(response, 'form', 'some_field', 'This field is required.')
        # etc. ...

    def test_call_view_fail_invalid(self):
        # as above, but with invalid rather than blank data in dictionary

    def test_call_view_success_invalid(self):
        # same again, but with valid data, then
        self.assertRedirects(response, '/contact/1/calls/')

显然,这里的缺点是URL的硬编码。您既可以reverse()在测试中使用,也可以使用建立RequestFactory视图并将其作为方法(而不是通过URL)调用请求。但是,使用后一种方法,您仍然需要使用硬编码值或reverse()测试重定向目标。

希望这可以帮助。


花了我几天时间让所有这些运行。再次感谢您的出色解释。我似乎无法运行的唯一测试是最后一个(test_call_view_success),即使我将所有必填字段都作为发布数据传递,它仍然抛出200而不是302。因此该测试失败。那我该如何测试成功呢?请参阅更新的问题。谢谢
Houman 2012年

3
您可以print response.context['form'].errors看到为什么form.is_valid()返回False。如果您需要进一步的帮助,请发布的代码CallsForm
supervacuo 2012年

关于使视图测试更像单元测试而不太像集成测试,则可以独立于urls.py利用SimpleTestCase.urls进行视图测试。同样,您可以通过制作测试模板并覆盖TEMPLATE_DIRS来使视图测试独立于模板。
cjerdonek 2014年

感谢@supervacuo这个好答案。有一个问题在想:是否应该从视图而不是内部视图对表单进行测试?
David D.

@大卫好主意!我通常从大型测试开始,这些测试打了很多东西,然后尝试从那里减少。最终,是的,我认为应该分别测试表单逻辑(希望主要是验证器逻辑?)。关于在视图测试期间提供虚拟表单数据的任何建议?
supervacuo 2014年

7

Django附带了一个测试客户端,该客户端可用于测试完整的请求/响应周期:文档包含一个示例,该示例向给定的url发出get请求,并声明状态代码以及模板上下文。您还需要一个测试,该测试可以执行POST并声明预期的成功重定向。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.