Django rest APITestCase 客户端在单元测试中将 null 布尔值转换为 false

问题描述

我在测试存在非空值的端点时遇到问题:

class Status(models.Model):
    """Class to represent the status of an Item. """

    name = models.TextField()

    description = models.TextField(null=True)

    color = models.TextField(null=True)

    in_possession = models.BooleanField()

    class Meta: # pylint: disable=too-few-public-methods
        """Class to represent Metadata of the object."""
        ordering = ['pk']

    def __str__(self):
        """String for representing the Model object."""
        return str(self.name)

我序列化并添加一个具有通用 ModelViewModelSerializer 的视图:

class StatusSerializer(serializers.ModelSerializer):
    """Serializer for Status."""

    class Meta: # pylint: disable=too-few-public-methods
        """Class to represent Metadata of the object."""
        model = Status
        fields = [ 'id','name','description','color','in_possession']
class StatusViewset(viewsets.ModelViewSet): # pylint: disable=too-many-ancestors
    """API Endpoint to return the list of status"""
    queryset = Status.objects.all()
    serializer_class = StatusSerializer
    permission_classes = (IsAuthenticated,IsFullUserOrReadOnly)
    pagination_class = None

现在我想对必填字段进行单元测试,所以我创建了这个子程序:

   def get_new_status(self,seed):
        """ This method returns the first status in the fixture"""
        return {
            "name": "name" + str(seed),"description": "description."  + str(seed),"color": "#32a852","in_possession": True
        }
    def test_mandatory_fields(self):
        """
        Test that the user can not create a status if a mandatory field is missing.
        """

        tests = ['name','in_possession']
        for test in tests:
            self.client.force_login(user=self.full_user)
            data = self.get_new_status('test_fields')
            expected = {
                test: [
                    "This field is required."
                ]
            }
            del data[test]
            response = self.client.post(STATUS_PATH,data=data,HTTP_AUTHORIZATION="Token " + self.token_full_user.key)
            print(response.status_code)
            print(response.data)
            self.assertEqual(response.status_code,status.HTTP_400_BAD_REQUEST)
            self.assertEqual(response.data,expected)

我遍历一个数组,它按预期工作,我进行打印,我可以在发送请求之前检查布尔字段是否已从 data删除,但在布尔场景中,状态已创建,我得到:

AssertionError: 201 != 400

我错过了什么吗?如果我对 insomnia(如邮递员)执行相同的请求,我会得到正确的验证:

{
  "in_possession": [
    "This field is required."
  ]
}

所以这让我觉得客户有问题,但我不知道如何进一步调查。

更新:我试图进一步简化它:

    def test_mandatory_fields(self):
        """
        Test that the user can not create a status if a mandatory field is missing.
        """


        self.client.force_login(user=self.full_user)
        data = self.get_new_status('test_fields')
        expected = {
            "in_possession": [
                "This field is required."
            ]
        }
        seed = 1
        data = {
        "name": "NewStatus" + str(seed),"description": "This status represents items that are in possession and still in use or there is no intention of getting rid of them."  + str(seed),"color": "#32a852"
        # "in_possession": True
        }
        print("DATA TO SEND")
        print(data)
        response = self.client.post(STATUS_PATH,HTTP_AUTHORIZATION="Token " + self.token_full_user.key)
        print(response.status_code)
        print(response.data)
        self.assertEqual(response.status_code,status.HTTP_400_BAD_REQUEST)
        self.assertEqual(response.data,expected)

结果还是一样。

更新 2: 以下来自@bdbd 的提示进一步调试:

我已经用 serializers.ModelSerializer 的打印件打印和覆盖方法几个小时了。似乎遵循以下步骤:

  1. is_valid
  2. run_validation
  3. to_internal_value 所以这个方法有一些打印:
    def to_internal_value(self,data):
        """
        Dict of native values <- Dict of primitive datatypes.
        """
        if not isinstance(data,Mapping):
            message = self.error_messages['invalid'].format(
                datatype=type(data).__name__
            )
            raise ValidationError({
                APISettings.NON_FIELD_ERRORS_KEY: [message]
            },code='invalid')

        ret = OrderedDict()
        errors = OrderedDict()
        fields = self._writable_fields

        for field in fields:
            validate_method = getattr(self,'validate_' + field.field_name,None)
            print(field.field_name)
            primitive_value = field.get_value(data)
            print('primitive_value is...')
            print(primitive_value)
            try:
                validated_value = field.run_validation(primitive_value)
                print('validated_value is..')
                print(validated_value)
                if validate_method is not None:
                    print('Is not none...')
                    validated_value = validate_method(validated_value)
                    print('validation_value is...')
                    print(validated_value)
            except ValidationError as exc:
                print('except1')
                errors[field.field_name] = exc.detail
            except DjangovalidationError as exc:
                print('except2')
                errors[field.field_name] = get_error_detail(exc)
            except SkipField:
                print('except3')
                pass
            else:
                print('except4')
                print('source_attrs')
                print(field.source_attrs)
                print('validated_value')
                print(validated_value)
                print('before_set_value')
                print(ret)
                set_value(ret,field.source_attrs,validated_value)
                print('after_set_value')
                print(ret)

        if errors:
            print('ValidationError...')
            raise ValidationError(errors)

        print('returning ret...')
        print(ret)
        print('...')
        return ret

这是我使用 Insomina 执行 REST 查询时的输出

...
OrderedDict([('name','Active_3'),('description',None)])
color
primitive_value is...
#00ff00
validated_value is..
#00ff00
except4
source_attrs
['color']
validated_value
#00ff00
before_set_value
OrderedDict([('name',None)])
after_set_value
OrderedDict([('name',None),('color','#00ff00')])
in_possession
primitive_value is...
<class 'rest_framework.fields.empty'>
except1
ValidationError...
Bad Request: /api/v1/status/
[05/Jun/2021 01:08:56] "POST /api/v1/status/ HTTP/1.1" 400 45

当我通过测试执行它时:

primitive_value is...
#32a852
validated_value is..
#32a852
except4
source_attrs
['color']
validated_value
#32a852
before_set_value
OrderedDict([('name','NewStatustest_fields'),'This status represents .test_fields')])        
after_set_value
OrderedDict([('name','This status represents .test_fields'),'#32a852')])
in_possession
primitive_value is...
False
validated_value is..
False
except4
source_attrs
['in_possession']
validated_value
False
before_set_value
OrderedDict([('name','#32a852')])
after_set_value
OrderedDict([('name','#32a852'),('in_possession',False)])
returning ret...
OrderedDict([('name','This status represents ....test_fields'),False)])
...
value is...
OrderedDict([('name','This status represents '),False)])
after run validation...
OrderedDict([('name',False)])
OrderedDict([('name',False)])
FAIL
test_update_status (test_status.StatusAPI)

正如您在打印 primitive_value is... 后看到的,由于某种奇怪的原因,我得到了不同的结果,我不知道如何从那里继续。

解决方法

我建议您在模型 default 上定义 null=True 或设置 BooleanField。这可能会解决奇怪的行为。

您很可能在前端使用 HTML 复选框作为布尔值。浏览器将省略所有未选中的复选框,只留下真正的参数。因此后端验证可能会认为所有不存在的参数都是假的。

我还没有测试过上面的建议,但我想如果你只是设置默认值或自己处理空的布尔参数而不是在一个错误的请求中调用一个缺少的布尔值,你会改进你的代码。

    in_possession = models.BooleanField(default=False)