如何为包含disabled只读字段的Django ProfileEditForm编写单元测试?
你的测试失败的核心原因是:Django表单中设置了disabled=True的字段,在提交时会被自动排除在表单的验证数据之外——哪怕你在form_data里传了这些字段的值,表单也不会读取它们。而你的username和email字段默认是required=True(没有显式设置required=False),所以当表单没有从instance获取这些字段的值时,就会触发“必填字段”的验证错误。
而实际项目中用UpdateView正常工作,是因为UpdateView会自动把当前的Profile实例传入表单,表单会从instance中读取username和email的初始值,不需要依赖提交的数据。
下面是正确的单元测试写法,覆盖两种核心测试场景:
1. 测试字段确实处于禁用状态
首先要验证表单初始化后,username和email字段的widget确实带有disabled属性,确保前端无法修改:
def test_disabled_fields_are_marked(self): # 传入instance,模拟UpdateView的场景 form = ProfileEditForm(instance=self.profile) # 检查字段的widget是否包含disabled属性 self.assertIn('disabled', form.fields['username'].widget.attrs) self.assertIn('disabled', form.fields['email'].widget.attrs)
2. 测试禁用字段无法被修改
验证即使在提交数据中篡改了username和email的值,表单保存后这些字段依然保持原值:
def test_disabled_fields_cannot_be_altered(self): form_data = { # 修改可编辑字段 "first_name": "UpdatedFirst", "last_name": "UpdatedLast", "about_me": "Updated about me text", # 尝试篡改禁用字段 "username": "malicious_user", "email": "hacker@example.com" } # 必须传入instance,让表单从实例读取禁用字段的初始值 form = ProfileEditForm(data=form_data, instance=self.profile) # 验证表单整体有效 self.assertTrue(form.is_valid()) # 保存表单 updated_profile = form.save() # 验证禁用字段没有被修改 self.assertEqual(updated_profile.user.username, "testuser") self.assertEqual(updated_profile.user.email, "test@test.com") # 验证可编辑字段正确更新 self.assertEqual(updated_profile.first_name, "UpdatedFirst") self.assertEqual(updated_profile.last_name, "UpdatedLast") self.assertEqual(updated_profile.about_me, "Updated about me text")
3. 测试表单验证不依赖禁用字段的提交数据
验证即使不在form_data中传入username和email,表单依然能通过验证(因为会从instance读取):
def test_form_valid_without_disabled_fields_in_data(self): form_data = { "first_name": "TestFirst", "last_name": "TestLast", # 完全不传username和email } form = ProfileEditForm(data=form_data, instance=self.profile) self.assertTrue(form.is_valid())
为什么你的原始测试失败?
你之前的测试没有传入instance参数,表单会被当作新建表单来处理。此时username和email字段因为被标记为disabled,提交的数据会被忽略,而这两个字段又没有默认值且是必填的,所以表单验证失败。
而实际项目中的UpdateView会自动将当前对象作为instance传入表单,所以表单能从实例中获取username和email的值,不会触发必填错误。
内容的提问来源于stack exchange,提问作者black

