Django: как смоделировать класс в представлении API

Название может быть немного запутанным.

Скажем, у меня есть APIView с методом post. Внутри метода post я представил класс, у которого есть собственный метод. В данном случае это класс, который занимается загрузкой на S3, что я хочу пропустить при запуске unittest.

class SomeView(APIView):
    def post(self):
        # do something here
        input1 = some_process(payload_arg1)
        input2 = some_other_process(payload_arg2)
        uploader = S3Uploader()
        s3_response = uploader.upload_with_aux_fxn(input1, input2)
        if s3_response['status_code'] == 200:
            # do something else
            return Response('Good job I did it!', status_code=200)
        else:
            return Response('noooo you're horrible!', status_code=400)

Очевидно, что реальный код имеет разные вызовы функций и ответы.

Теперь мне нужно издеваться над этими uploader и uploader.upload_with_aux_fxn, поэтому я на самом деле не вызываю S3. Как мне издеваться над этим?

Я пробовал в своем тестовом сценарии

from some_place import S3Uploader
class SomeViewTestCase(TestCase): 
    def setUp(self):        
        self.client = APIClient()
        uploader_mock = S3Uploader()
        uploader_mock.upload_support_doc = MagicMock(return_value={'status_code': 200, 'message': 'asdasdad'}
        response = self.client.post(url, payload, format='multipart')

Но я все равно запустил загрузку S3 (поскольку файл отображается на S3). Как мне правильно издеваться над этим?

РЕДАКТИРОВАТЬ1:

моя попытка пропатчить

def setUp(self):
    self.factory = APIRequestFactory()
    self.view = ViewToTest.as_view()
    self.url = reverse('some_url')


@patch('some_place.S3Uploader', FakeUploader)
def test_uplaod(self):
    payload = {'some': 'data', 'other': 'stuff'}
    request = self.factory.post(self.url, payload, format='json')
    force_authenticate(request, user=self.user)
    response = self.view(request)

где находится FakeUplaoder

class FakeUplaoder(object):
    def __init__(self):
        pass
    def upload_something(self, data, arg1, arg2, arg3):
        return {'status_code': 200, 'message': 'unit test', 's3_path': 
                'unit/test/path.pdf'}

    def downlaod_something(self, s3_path):
        return {'status_code': 200, 'message': '', 'body': 'some base64 
                stuff'}

к сожалению, это не удается. Я все еще попал в настоящий класс

РЕДАКТИРОВАТЬ 2:

Я использую Django 1.11 и Python 2.7, на случай, если людям понадобится эта информация.


person JChao    schedule 05.07.2019    source источник


Ответы (4)


Я предполагаю, что правильным подходом к этому будет сохранение файла в модели с помощью FileField, а затем подключение Boto для обработки загрузки в производственном сценарии.
Внимательно посмотрите на:
https://docs.djangoproject.com/en/2.2/ref/models/fields/#filefield
и
https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#model
такой подход сохранит поведение Django по умолчанию, что сделает работу с тестовым клиентом Django по умолчанию более удобным для тестирования.

person João Victor Monte    schedule 09.07.2019
comment
не сохраняет FileField локально? Что произойдет, если у меня будет несколько серверов для обработки производственных данных? Может ли django обеспечить доступ ко всем файлам? (Я сталкивался с подобными проблемами, прежде чем поверить. Иногда данные, сохраненные на сервере, недоступны, потому что они находятся на другом сервере) - person JChao; 10.07.2019
comment
Нет. Вы можете установить переменную для условной загрузки конфигураций производственной среды. Чтобы загрузить свои медиафайлы на S3, установите: DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'. Подробнее: django-storages.readthedocs.io/en/ последние/бэкенды/ - person João Victor Monte; 10.07.2019
comment
но как я могу сделать это, не проходя через это? Все, что я хочу, это издеваться над этим классом, работающим с S3Upload, и подделывать ответ. Это отдельный класс со своими функциями. Он находится внутри файла django APIView. Мне просто нужно поиздеваться над этим. Это все - person JChao; 10.07.2019
comment
На самом деле я не могу думать о поддерживаемом способе сделать это без стандартного поведения Django. - person João Victor Monte; 10.07.2019

Взгляните на vcrpy. Он записывает запрос к внешнему API один раз, а затем воспроизводит ответ каждый раз, когда вы запускаете свои тесты. Не нужно вручную издеваться над чем-либо.

person r_black    schedule 09.07.2019
comment
есть ли способ сделать это только с помощью unittest? мне не разрешено добавлять новую библиотеку, когда я захочу. - person JChao; 10.07.2019
comment
Тогда вам следует придерживаться насмешек. ИМХО, Воссоздание такого поведения потребовало бы слишком большого патчинга внутренностей бото, оно того не стоит. - person r_black; 15.07.2019

Попробуйте использовать MagicMock, как показано ниже.

from unittest import mock
from storages.backends.s3boto3 import S3Boto3Storage


class SomeTestCase(TestCase):
  def setUp(self):
    self.factory = APIRequestFactory()
    self.view = ViewToTest.as_view()
    self.url = reverse('some_url')


  @mock.patch.object(S3Boto3Storage, '_save', MagicMock(return_value='/tmp/somefile.png'))
  def test_uplaod(self):
    payload = {'some': 'data', 'other': 'stuff'}
    request = self.factory.post(self.url, payload, format='json')
    force_authenticate(request, user=self.user)
    response = self.view(request)
person anjaneyulubatta505    schedule 11.07.2019
comment
это не работает. он по-прежнему попадает на S3 и возвращает Unexpected Error: Parameter validation failed: Invalid type for parameter Body - person JChao; 12.07.2019
comment
Эта ошибка, вероятно, связана с тем, что я использую json вместо multipart, тем не менее, смысл не в том, чтобы попасть в S3, поэтому я вообще не должен получить эту ошибку. - person JChao; 12.07.2019

Вот пример того, как я бы издевался над этим S3Uploader в APITestCase.

from rest_framework import status
from unittest import mock
from unittest.mock import MagicMock

class SomeViewTestCase(APITestCase): 

   @mock.patch("path.to.view_file.S3Uploader")
   def test_upload(self, s3_uploader_mock):
       """Test with mocked S3Uploader"""
       concrete_uploader_mock = MagicMock(**{
           "upload_with_aux_fxn__return_value": {"status_code": 200}
       })
       s3_uploader_mock.return_value = concrete_uploader_mock
       response = self.client.post(url, payload, format='multipart')
       self.assertEqual(response.status_code, status.HTTP_200_OK)
       s3_uploader_mock.assert_called_once()
       concrete_uploader_mock.upload_with_aux_fx.assert_called_once()
person A. J. Parr    schedule 12.07.2019
comment
мне нужно определить s3_uploader_mock перед его использованием в def test_upload? - person JChao; 12.07.2019
comment
Я попробовал это с изменением mock.patch с фактическим путем. Также изменил return_value на значение, которое я хочу. К сожалению, он все еще попадает в S3 - person JChao; 12.07.2019
comment
Нет, вам не нужно определять его заранее, после того, как вы добавите декоратор mock.patch, он должен включать аргумент в вашу тестовую функцию, которая является издевательским классом/объектом. Я немного удивлен, узнав, что он все еще поражает S3 с этим макетом на месте, что указывает на то, что @mock.patch не нацелен на правильный объект. Нужно будет увидеть больше кода просмотра, чтобы диагностировать, почему это происходит. - person A. J. Parr; 15.07.2019
comment
Вы также можете попробовать настроить таргетинг на модуль, из которого вы импортируете S3Uploader, например. @mock.patch("path.to.s3_module.S3Uploader") вместо модуля представлений. - person A. J. Parr; 15.07.2019