Django测试ModelForm时遭遇ValidationError:非有效UUID及表单初始化参数缺失问题排查
让我们一步步拆解并解决你遇到的两个问题:
问题1:TypeError: __init__() missing 1 required positional argument: 'user'
你的CourseForm在构造方法里把user设为了第一个必填参数,但测试代码调用表单时直接传了data,完全没给user参数,Python自然会报参数缺失的错误。
问题2:ValidationError: ["“{...}” is not a valid UUID."]
当你尝试把request.POST直接传给CourseForm时,这个POST字典被当成了第一个参数(也就是user参数)。而Course模型的user字段是外键,Django会尝试把这个字典解析成用户实例的UUID主键,字典肯定不是有效的UUID格式,所以触发了验证错误。
完整解决方案
1. 修复测试代码,正确初始化表单
测试时必须先传递user参数,再通过data关键字传递表单数据;如果是测试更新场景,还要传入instance参数(因为UpdateView是针对现有对象的更新操作):
class CourseUpdateTests(TestCase): @classmethod def setUpTestData(cls): cls.user = CustomUser.objects.create_user( username='tester', email='tester@email.com', password='tester123', is_teacher=True, is_active=True, ) # 先创建一个待更新的课程实例 cls.course = Course.objects.create( user=cls.user, course_name="Old Science", grade_level="SEC" ) def test_CourseUpdate_valid(self): # 正确传递user参数,用data传表单数据,instance指定要更新的对象 form = CourseForm( user=self.user, data={ 'course_name': "Science", 'grade_level': "SEC" }, instance=self.course ) self.assertTrue(form.is_valid())
注意:不需要在data里传user和id——user已经通过表单构造参数传入,id是主键由instance自动携带,而且你的表单fields里也没包含这两个字段。
2. 优化表单的clean方法(避免更新时误判重名)
当前的clean方法会阻止用户把课程名改成自己已有的任何课程名,但如果是更新现有课程,用户只是修改年级、保留原名,这也会触发错误。所以需要在查询时排除当前实例:
class CourseForm(ModelForm): class Meta: model = Course fields = ('course_name', 'grade_level',) def __init__(self, user, *args, **kwargs): super().__init__(*args, **kwargs) self.user = user # 把user存为实例属性,方便后续使用 self.qc = Course.objects.filter(user=user) def clean(self): super().clean() course_name = self.cleaned_data.get('course_name') if course_name: # 如果是更新场景,排除当前要修改的课程实例 queryset = self.qc.exclude(pk=self.instance.pk) if self.instance else self.qc if queryset.filter(course_name__iexact=course_name).exists(): raise ValidationError("A course with that name already exists.") if len(course_name) > 20: raise ValidationError("Your course name cannot be longer than 20 characters") return self.cleaned_data
这里做了两个关键优化:
- 把
user保存为实例属性,避免重复查询 - 更新时排除当前实例,允许用户保留原名
3. 确保视图正确使用自定义表单
你的CourseUpdateView目前用的是默认ModelForm,需要指定使用你自定义的CourseForm,并传递user参数:
@method_decorator([login_required, teacher_required], name='dispatch') class CourseUpdateView(PermissionRequiredMixin, UpdateView): raise_exceptions = True permission_required = 'gradebook.change_course' permission_denied_message = "You don't have access to this." model = Course form_class = CourseForm # 指定自定义表单 template_name_suffix = '_update' def get_form_kwargs(self): # 把当前登录用户传给表单的构造方法 kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def get_success_url(self, *args, **kwargs): return reverse('gradebook:coursedetail', kwargs={'course_pk': self.object.pk})
原来的fields参数可以删掉了,因为form_class已经指定了自定义表单,表单里已经定义了fields。
现在你的测试应该能正常通过,同时视图也能正确使用带重名校验的自定义表单了。
内容的提问来源于stack exchange,提问作者Doug Smith

