简体   繁体   English

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

[英]Django rest APITestCase client converts null booleans into false in unit tests

I'm having troubles testing an endpoint where there is a non null value:我在测试没有 null 值的端点时遇到问题:

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)

Which I serialize and add a view with the common ModelView and ModelSerializer :我使用常见的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

And now I want to unit test the mandatory fields, so I created this subroutine:现在我想对必填字段进行单元测试,所以我创建了这个子例程:

   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)

I loop through an array and it works as expected, I make prints and I can check the boolean field has been deleted from data before sending the request, but in the boolean scenario the Status is created and I get:我循环遍历一个数组,它按预期工作,我进行打印,我可以在发送请求之前检查 boolean 字段是否已从data中删除,但在 boolean 场景中,状态已创建,我得到:

AssertionError: 201 != 400

Am I missing something?我错过了什么吗? If I do the same request with a insomnia (like postman) I get the right validation:如果我对insomnia做同样的请求(比如邮递员),我会得到正确的验证:

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

So this makes me think there is something wrong with the client but I have no idea how can I further investigate this.所以这让我觉得客户有问题,但我不知道如何进一步调查。

UPDATE: I tried to simplify it even more:更新:我试图进一步简化它:

    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, 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)

Still the same result.结果还是一样。

Update 2 : Further debugging following hints from @bdbd:更新 2 :进一步调试来自@bdbd 的提示:

I have been some hours printing and overwriting methods with prints from serializers.ModelSerializer .我已经用serializers.ModelSerializer的打印打印和覆盖了几个小时的方法。 It seems to follows this steps:它似乎遵循以下步骤:

  1. is_valid
  2. run_validation
  3. to_internal_value So this method with some prints: 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

So this is the output when I execute the REST query with Insomina:所以当我使用 Insomina 执行 REST 查询时,这是 output:

...
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', 'Active_3'), ('description', None)])
after_set_value
OrderedDict([('name', 'Active_3'), ('description', 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

And when I execute it through the Test:当我通过测试执行它时:

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

As you can see after the print primitive_value is... I get different results from some strange reason, I have no idea how to continue from there.正如您在 print primitive_value is...之后看到的那样,由于某种奇怪的原因,我得到了不同的结果,我不知道如何从那里继续。

I recommend you either define a default or set null=True on your models BooleanField .我建议您在模型BooleanField上定义default或设置null=True That might solve the weird behavior.这可能会解决奇怪的行为。

You most likely use a HTML checkbox for the boolean value on the frontend.您很可能在前端的 boolean 值中使用 HTML 复选框。 The browser will omit all check boxes that are unchecked, leaving you with just the true parameters.浏览器将忽略所有未选中的复选框,只留下真正的参数。 Therefor the backend validation might consider all non-existent parameters to be false.因此后端验证可能会认为所有不存在的参数都是错误的。

I haven't tested the above recommendation, but I guess you'll improve your code if you just set the default or handle empty boolean parameters yourself instead calling a missing boolean in a form a bad request.我没有测试上述建议,但我想如果您只是设置默认值或自己处理空的 boolean 参数,而不是以错误请求的形式调用缺少的 boolean 参数,您会改进代码。

    in_possession = models.BooleanField(default=False)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM