Тестирование загрузки изображений в Django REST Framework

Я пытаюсь добавить несколько тестов к одному из моих приложений в проекте Django. Это приложение включает в себя Image модель:

from django.db import models
from django.utils.translation import gettext_lazy as _

from cecommerce.image_mappings import ImageAgnosticMapping
from cecoresizer.fields import ResizableImageField


class Image(models.Model):
    file = ResizableImageField(
        _("Image"),
        max_length=300,
        image_config=ImageAgnosticMapping,
    )

    def __str__(self):
        return str(self.file)

Затем он сериализуется с помощью следующего сериализатора:

from django.http import Http404

from rest_framework import serializers

from cecotec_apps.landings.models import ProductLanding
from cecotec_apps.partner.models import Home

from images.models import Image


class ImageSerializer(serializers.ModelSerializer):
    file = serializers.ImageField()

    def to_representation(self, instance):
        return {"file": str(instance)}

    class Meta:
        model = Image
        exclude = ("id",)

Я пытаюсь проверить создание экземпляра Image (который работает при запросе API с помощью Imsonia) с помощью этого теста:

import tempfile

from PIL import Image as ImageFile

from django.test import tag

from model_bakery import baker

from rest_framework.test import APITestCase
from rest_framework.authtoken.models import Token
from rest_framework.reverse import reverse


from cecotec_apps.landings.models import ProductLanding
from cecotec_apps.partner.models import Home

from images.api.serializers import ImageSerializer
from images.models import Image

from tests.oscar.decorator.decorator_all_methods import global_test_decorator

from user.models import User


@tag("e2e", "image")
@global_test_decorator()
class ImageTestCase(APITestCase):
    API_VERSION = "v1"
    IMAGES_QUANTITY = 20
    HTTP_HOST = "localhost:8000"

    @classmethod
    def _populate(cls):
        cls.token = baker.make(Token)
        baker.make_recipe("images.image_recipe", _quantity=cls.IMAGES_QUANTITY)

    @classmethod
    def _generate_image_file(cls):
        with tempfile.NamedTemporaryFile(suffix="jpg") as tmp_file:
            image = ImageFile.new("RGB", size=(100, 100))
            image.save(tmp_file, "jpeg")
            tmp_file.seek(0)

            return tmp_file

    @classmethod
    def _depopulate(cls):
        Token.objects.all().delete()
        User.objects.all().delete()
        ProductLanding.objects.all().delete()
        Home.objects.all().delete()
        Image.objects.all().delete()

    def setUp(self, *args):
        self._depopulate()
        self._populate()

    def tearDown(self, *args):
        self._depopulate()

    @tag("create", "authenticated")
    def test_create_authenticated(self, *args):
        self.client.force_authenticate(user=self.token.user, token=self.token)

        data = self._prepare_create_data(**{"file": self._generate_image_file()})

        response = self._call_create(data)

        self._assert_create(response)
        self.assertRegex(response.data.get("file"), "^images\/.+$")

    def _call_create(self, data):
        return self.client.post(
            path=reverse(f"image-list"),
            data=data,
            # content_type=MULTIPART_CONTENT,
            format="multipart",
            HTTP_HOST=self.HTTP_HOST,
        )

    def _assert_create(self, response, expected_status_code=201):
        self.assertEqual(response.status_code, expected_status_code)

        if expected_status_code == 201:
            response_data = response.data

            self.assertEqual(Image.objects.count(), self.IMAGES_QUANTITY + 1)
            self._assert_response_body(response_data)
        else:
            self.assertEqual(Image.objects.count(), self.IMAGES_QUANTITY)

    def _assert_response_body(self, response_body):
        self.assertIn("file", response_body)

Но при запуске код ответа возвращается 400 Bad Request вместо 201 Created. Тело ответа выглядит следующим образом:

{
    "file": [
        ErrorDetail(string="The submitted data was not a file. Check the encoding type on the form.",
        code="invalid")
    ]
}

Я также пробовал использовать уже созданный файл для тестирования, но ошибка была той же самой. Я видел много сообщений в Интернете, но ни одно из них не помогло мне решить эту проблему.

Буду признателен за помощь. Заранее спасибо.

Причина

Я думаю, что проблема возникает из-за метода _generate_image_file.

@classmethod
def _generate_image_file(cls):
    with tempfile.NamedTemporaryFile(suffix="jpg") as tmp_file:
        image = ImageFile.new("RGB", size=(100, 100))
        image.save(tmp_file, "jpeg")
        tmp_file.seek(0)

        return tmp_file

Поскольку tempfile.NamedTemporaryFile создается с помощью оператора with, то к тому времени, когда вы вернули его и использовали в другом методе, файл уже закрыт и удален.

Решение

Вам следует изменить _generate_image_file на менеджер контекста, используя украшение метода с помощью contextlib.contextmanager и использовать оператор yied вместо return.

from contextlib import contextmanager

@contextmanager
@classmethod
def _generate_image_file(cls):
    with tempfile.NamedTemporaryFile(suffix="jpg") as tmp_file:
        image = ImageFile.new("RGB", size=(100, 100))
        image.save(tmp_file, "jpeg")
        tmp_file.seek(0)

        yied tmp_file

А в методе test_create_authenticated измените его на следующий

@tag("create", "authenticated")
def test_create_authenticated(self, *args):
    ...
    with self._generate_image_file() as image_file:
        data = self._prepare_create_data(**{"file": image_file})
        response = self._call_create(data)
    ...

Теперь файл изображения будет удален после выхода из блока with.

Я бы рекомендовал использовать следующее:

from django.core.files.uploadedfile import SimpleUploadedFile

with open("/some/path/to/image.jpg", "rb") as image:
        image = SimpleUploadedFile("image.jpg", image.read(), content_type="image/jpg")
        data = self._prepare_create_data(**{"file": image})
...

Что происходит, так это то, что ваш сериализатор проверяет, что отправляемый файл не является действительным. Убедитесь, что parser_classes = (parsers.MultiPartParser,) в вашем представлении, models.ImageField также в вашей модели.

Вернуться на верх