簡體   English   中英

Django Rest 框架給了我使用單元測試時不應該出現的驗證錯誤

[英]Django Rest Framework give me validation error that shouldn't be raised when using Unit Tests

我正在創建這個 API 並且在我的測試的確定點(合同創建端點)我收到一個無效錯誤。

該錯誤表明我在創建合同時沒有將一些必需的屬性傳遞給 API,但我是。 最奇怪的是,當我嘗試從 Web 瀏覽器手動創建時,沒有提出問題並創建了合約

我在這里放了很多代碼只是為了復制目的,但真正重要的是看到它的代碼是ContractSerializertest_contract_creation function

這是我的代碼:

models.py

from django.core.exceptions import ValidationError
from django.db import models
from django.core.validators import (
    MaxValueValidator,
    MinValueValidator,
    RegexValidator,
)

from core.validators import GreaterThanValidator, luhn_validator


class Planet(models.Model):
    name = models.CharField(
        max_length=50, unique=True, null=False, blank=False
    )

    def __str__(self) -> str:
        return self.name


class Route(models.Model):
    origin_planet = models.ForeignKey(
        Planet,
        on_delete=models.CASCADE,
        related_name="origin",
    )
    destination_planet = models.ForeignKey(
        Planet,
        on_delete=models.CASCADE,
        related_name="destination",
    )
    fuel_cost = models.IntegerField(validators=[GreaterThanValidator(0)])

    class Meta:
        unique_together = (("origin_planet", "destination_planet"),)

    def clean(self) -> None:
        # super().full_clean()
        if self.origin_planet == self.destination_planet:
            raise ValidationError(
                "Origin and destination planets must be different."
            )

    def save(self, *args, **kwargs):
        self.full_clean()
        super().save(*args, **kwargs)

    def __str__(self) -> str:
        return f"{self.origin_planet} - {self.destination_planet}"


class Ship(models.Model):
    ship_model = models.CharField(max_length=50, null=False, unique=True)
    fuel_capacity = models.IntegerField(validators=[GreaterThanValidator(0)])
    weight_capacity = models.IntegerField(validators=[GreaterThanValidator(0)])

    def __str__(self) -> str:
        return self.ship_model


class Pilot(models.Model):
    name = models.CharField(max_length=50, null=False)
    age = models.IntegerField(
        validators=[MinValueValidator(18), MaxValueValidator(60)]
    )
    certification = models.CharField(
        max_length=7,
        validators=[
            RegexValidator(
                r"^[0-9]{7}$", "Only digit characters and length 7"
            ),
            luhn_validator,
        ],
        null=False,
        blank=False,
        unique=True,
    )
    credits = models.PositiveIntegerField(default=0, editable=False)
    location_planet = models.ForeignKey(Planet, on_delete=models.CASCADE)
    ships = models.ManyToManyField(Ship, through="Ownership")

    def __str__(self) -> str:
        return f"{self.name}:{self.certification}"


class Ownership(models.Model):
    """Third table for Pilot and Ship relation"""

    pilot = models.ForeignKey(Pilot, on_delete=models.CASCADE)
    ship = models.ForeignKey(Ship, on_delete=models.CASCADE)
    fuel_level = fuel_level = models.PositiveIntegerField(
        default=100, validators=[MaxValueValidator(100)]
    )

    def __str__(self) -> str:
        return f"{self.pilot} -> {self.ship} fuel_level:{self.fuel_level}"


class Resource(models.Model):
    name = models.CharField(max_length=50, unique=True)

    def save(self, *args, **kwargs):
        self.name = self.name.lower()
        super().save(*args, **kwargs)

    def __str__(self) -> str:
        return self.name


class Contract(models.Model):
    description = models.CharField(max_length=50, null=False)
    route = models.ForeignKey(Route, on_delete=models.CASCADE)
    value = models.IntegerField(validators=[GreaterThanValidator(0)])
    completed = models.BooleanField(default=False)
    resources = models.ManyToManyField(Resource, through="Cargo")

    def cargo_weight(self) -> int:
        return sum([cargo.weight for cargo in self.cargo_set.all()])

    def __str__(self) -> str:
        return f"{self.route} contract - R${self.value}"


class Cargo(models.Model):
    resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
    contract = models.ForeignKey(Contract, on_delete=models.CASCADE)
    weight = models.IntegerField(
        validators=[GreaterThanValidator(0)], null=False
    )

view.py

from django.forms.models import model_to_dict
from django.db.utils import IntegrityError

from rest_framework import viewsets
from rest_framework.reverse import reverse
from rest_framework.exceptions import ValidationError

from space_travel import serializers
from core import models


class ResourceViewSet(viewsets.ModelViewSet):
    queryset = models.Resource.objects.all()
    serializer_class = serializers.ResourceSerializer


class ShipViewSet(viewsets.ModelViewSet):
    queryset = models.Ship.objects.all()
    serializer_class = serializers.ShipSerializer


class PlanetViewSet(viewsets.ModelViewSet):
    queryset = models.Planet.objects.all()
    serializer_class = serializers.PlanetSerializer


class PilotViewSet(viewsets.ModelViewSet):
    queryset = models.Pilot.objects.all()
    serializer_class = serializers.PilotSerializer


class RouteViewSet(viewsets.ModelViewSet):
    queryset = models.Route.objects.all()

    def get_serializer_class(self):
        if self.action in ["retrieve", "list"]:
            # This serializer will show a hyperlinked
            # representation for origin/destination fields
            return serializers.RouteReadSerializer

        # This serializer will use th planet names for writing
        # instead of hyperlinked representations
        return serializers.RouteWriteSerializer


class ContractViewSet(viewsets.ModelViewSet):
    queryset = models.Contract.objects.all()
    serializer_class = serializers.ContractSerializer

serializers.py

from typing import Dict

from django.urls.base import reverse

from rest_framework import serializers
from rest_framework.reverse import reverse

from core import models


class ResourceSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Resource
        fields = "__all__"


class PlanetSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Planet
        fields = "__all__"


class RouteWriteSerializer(serializers.ModelSerializer):
    """This serializers will be used only for write operations
    like update, create"""

    origin_planet = serializers.SlugRelatedField(
        slug_field="name",
        queryset=models.Planet.objects.all(),
    )
    destination_planet = serializers.SlugRelatedField(
        slug_field="name",
        queryset=models.Planet.objects.all(),
    )

    class Meta:
        model = models.Route
        fields = "__all__"


class RouteReadSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Route
        fields = "__all__"


class PilotSerializer(serializers.HyperlinkedModelSerializer):
    location_planet = serializers.SlugRelatedField(
        many=False, queryset=models.Planet.objects.all(), slug_field="name"
    )
    ships = serializers.SlugRelatedField(
        queryset=models.Ship.objects.all(), many=True, slug_field="ship_model"
    )

    class Meta:
        model = models.Pilot
        fields = "__all__"

    def to_representation(self, instance: models.Pilot) -> Dict:
        """This method is responsible for represent the ship list
        in a payload mode (should access the ownership table):
            {
                url: {schema}://{domain}/{path to the ship},
                fuel_level: {fuel level} (comes from the ownership table)
            }
        """
        representation = super().to_representation(instance)
        representation["ships"] = []

        for ownership in instance.ownership_set.all():
            ship_url = reverse(  # Get the ship url
                "space_travel:ship-detail",
                args=[ownership.ship.pk],
                request=self._context["request"],
            )
            representation["ships"].append(
                {"url": ship_url, "fuel_level": ownership.fuel_level}
            )

        return representation


class ShipSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Ship
        fields = "__all__"


class CargoSerializer(serializers.ModelSerializer):
    resource = serializers.SlugRelatedField(
        queryset=models.Resource.objects.all(),
        slug_field="name",
        read_only=False,
    )

    class Meta:
        model = models.Cargo
        fields = ["resource", "weight"]


class ContractSerializer(serializers.HyperlinkedModelSerializer):
    origin_planet = serializers.SlugRelatedField(
        slug_field="name",
        queryset=models.Planet.objects.all(),
        many=False,
        write_only=True,
    )
    destination_planet = serializers.SlugRelatedField(
        slug_field="name",
        queryset=models.Planet.objects.all(),
        many=False,
        write_only=True,
    )
    route = RouteReadSerializer(many=False, read_only=True)
    cargo_set = CargoSerializer(many=True)

    class Meta:
        model = models.Contract
        fields = [
            "description",
            "value",
            "route",
            "origin_planet",
            "destination_planet",
            "cargo_set",
        ]

    def to_representation(self, instance: models.Contract) -> Dict:
        representation = super().to_representation(instance)
        representation["cargos"] = representation.pop("cargo_set")
        representation["total_weight"] = instance.cargo_weight()

        return representation

    def create(self, validated_data: Dict) -> models.Contract:
        route = models.Route.objects.get(
            origin_planet=validated_data.pop("origin_planet"),
            destination_planet=validated_data.pop("destination_planet"),
        )
        cargos = validated_data.pop("cargo_set")
        contract = models.Contract.objects.create(
            **validated_data, route=route
        )
        for cargo in cargos:
            contract.resources.add(
                cargo.pop("resource"), through_defaults=cargo
            )

        return contract

test_contract_endpoint.py

import random

from django.test import TestCase
from django.urls import reverse

from rest_framework import status
from rest_framework.test import APIClient

from core.models import Cargo, Contract, Planet, Resource, Route


def link_list(endpoint: str, domain: bool = False):
    link_path = reverse(f"space_travel:{endpoint}-list")
    return link_path if not domain else f"http://testserver{link_path}"


def link_details(endpoint: str, _id: int, domain: bool = False):
    link_path = reverse(f"space_travel:{endpoint}-detail", args=[_id])
    return link_path if not domain else f"http://testserver{link_path}"


class TestContractEndpoint(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.endpoints = {
            "contract": "contract",
            "route": "route",
            "planet": "planet",
        }

        self.sample_pl1 = Planet.objects.get_or_create(name="Andvari")[0]
        self.sample_pl2 = Planet.objects.get_or_create(name="Demeter")[0]

        self.sample_resources = [
            Resource.objects.create(name=f"resource {n}") for n in range(4)
        ]
        self.sample_route = Route.objects.create(
            origin_planet=self.sample_pl1,
            destination_planet=self.sample_pl2,
            fuel_cost=100,
        )
        self.sample_contract = Contract.objects.create(
            route=self.sample_route,
            value=1000,
            description="Contract to deliver water and food to Demeter",
        )
        self.sample_contract.resources.set(
            self.sample_resources, through_defaults={"weight": 500}
        )

    def test_contract_creation(self):
        """Test if the contract creation are automatically setting the route
        passing the origin and destination planets names and connecting the
        resources to the contract through the Cargo model."""

        resources = [
            Resource.objects.get_or_create(name="minerals")[0],
            random.choice(self.sample_resources),
        ]
        payload = {
            "description": "Contract to deliver minerals to Saturn",
            "origin_planet": self.sample_pl1.name,
            "destination_planet": self.sample_pl2.name,
            "value": 1000,
            "cargo_set": [
                {"resource": resources[0].name, "weight": 545},
                {"resource": resources[1].name, "weight": 876},
            ],
        }
        res = self.client.post(link_list(self.endpoints["contract"]), payload)

        self.assertEqual(res.status_code, status.HTTP_201_CREATED)

        contract = Contract.objects.filter(
            description=payload["description"],
            route=self.sample_route,
            value=payload["value"],
            resources__in=[resource.id for resource in resources],
        ).first()
        self.assertTrue(contract)

        filtered_cargos = Cargo.objects.filter(contract=contract)
        self.assertEqual(filtered_cargos.count(), 2)
        for idx, cargo in enumerate(filtered_cargos):
            self.assertEqual(cargo.weight, payload["cargo"][idx]["weight"])

現在這是我收到的驗證錯誤: {'cargo_set': [ErrorDetail(string='This field is required.', code='required')]}

這是我在 web 瀏覽器中手動創建的合同的打印,傳遞給它的有效負載與單元測試中的相同: 圖片

在對源代碼進行一些研究之后,我注意到我的cargo_set被實例化為ListSerializer ,問題是在field.get_value()內部調用.to_internal_data()時。 我實際上不知道為什么,但是parse_html_list()中的正則表達式不匹配。 這是我注意到的唯一可能是問題的地方

我會很高興有任何幫助。 謝謝!

這似乎是我遇到的同一個問題嵌套反序列化失敗並顯示“此字段是必需的”

確保使用發送請求 JSON 編碼的 APITest。

為了修復它,我更改了我的測試用例以使用 DRF 提供的 APIClient:

從 rest_framework.test 導入 APIClient

客戶端 = APIClient()

在我的 settings.py 中,我在配置中添加了以下內容:

REST_FRAMEWORK = {... 'TEST_REQUEST_DEFAULT_FORMAT': 'json' }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM