Django: Подражание вызову внешнего api в методе сохранения модели

Я хочу протестировать модельную форму с помощью pytest в двух режимах:

  1. без необходимости вызова внешнего API, используемого в методе сохранения
  2. генерируя ошибку, когда API не работает, чтобы я мог проверить созданную мной валидацию
  3. .

Вот мой код:

trips/models.py

class Place(models.Model):
trip = models.ForeignKey(Trip, on_delete=models.CASCADE, related_name="places")
day = models.ForeignKey(
    Day, on_delete=models.SET_NULL, null=True, related_name="places"
)
name = models.CharField(max_length=100)
url = models.URLField(null=True, blank=True)
address = models.CharField(max_length=200)
latitude = models.FloatField(null=True, blank=True)
longitude = models.FloatField(null=True, blank=True)

objects = models.Manager()
na_objects = NotAssignedManager()

def save(self, *args, **kwargs):
    old = type(self).objects.get(pk=self.pk) if self.pk else None
    # if address is not changed, don't update coordinates
    if old and old.address == self.address:
        return super().save(*args, **kwargs)
    g = geocoder.mapbox(self.address, access_token=settings.MAPBOX_ACCESS_TOKEN)
    self.latitude, self.longitude = g.latlng
    return super().save(*args, **kwargs)

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

trips/forms.py

class PlaceForm(forms.ModelForm):
class Meta:
    model = Place
    fields = ["name", "url", "address", "day"]
    formfield_callback = urlfields_assume_https
    widgets = {
        "name": forms.TextInput(attrs={"placeholder": "name"}),
        "url": forms.URLInput(attrs={"placeholder": "URL"}),
        "address": forms.TextInput(attrs={"placeholder": "address"}),
        "day": forms.Select(attrs={"class": "form-select"}),
    }
    labels = {
        "name": "Name",
        "url": "URL",
        "address": "Address",
        "day": "Day",
    }

def __init__(self, *args, parent=False, **kwargs):
    super().__init__(*args, **kwargs)
    if parent:
        trip = parent
    else:
        trip = self.instance.trip
    self.fields["day"].choices = (
        Day.objects.filter(trip=trip)
        .annotate(
            formatted_choice=Concat(
                "date",
                Value(" (Day "),
                "number",
                Value(")"),
                output_field=CharField(),
            )
        )
        .values_list("id", "formatted_choice")
    )
    self.helper = FormHelper()
    self.helper.form_tag = False
    self.helper.layout = Layout(
        Field("name"),
        Field("url"),
        Field("address"),
        "day",
    )

def clean_address(self):
    address = self.cleaned_data["address"]
    if not geocoder.mapbox(address, access_token=settings.MAPBOX_ACCESS_TOKEN):
        raise ValidationError("Cannot validate your address, please retry later")
    return address

Это пример теста, в котором я хочу поиздеваться над методом сохранения без вызова mapbox api

class TestPlaceForm:
def test_form(self, user_factory, trip_factory):
    """Test that the form saves a place"""
    user = user_factory()
    trip = trip_factory(author=user, title="Test Trip")
    day = trip.days.first()
    data = {
        "name": "Test Place",
        "address": factory.Faker("street_address"),
        "day": day,
    }
    form = PlaceForm(parent=trip, data=data)
    if form.is_valid():
        place = form.save(commit=False)
        place.trip = trip
        place.save()

    assert form.is_valid()
    assert place == trip.places.first()

и здесь код, чтобы закрыть шаг проверки недоступности mapbox

def test_no_mapbox_access_raise_validation_error(self, user_factory, trip_factory, place_factory):
    """ Test the form degrade gracefully when mapbox is not available"""
    user = user_factory()
    trip = trip_factory(author=user)
    place = place_factory(trip=trip)
    form = PlaceForm(instance=place)

    assert not form.is_valid()
    assert 'Cannot validate your address, please retry later' in form.errors['__all__']

Насколько я понимаю, я могу поиздеваться над методом сохранения или ответом геокодера, чтобы получить результат (поддельный или с ошибками), не спрашивая mapbox), но я не могу понять, как это сделать. Кто-нибудь может помочь?

Думаю, вы можете использовать декоратор python patch, который обрабатывает патч модуля

внутри файла юнит-тестов добавить

from unittest.mock import patch, Mock

mock_geocoder_response = Mock(latlng=(10.0, 20.0))

@pytest.fixture
def mocked_geocoder():
    with patch('trips.models.geocoder.mapbox', return_value=mock_geocoder_response) as mocked_geocoder:
        yield mocked_geocoder

#the test function will take the fixture as parameter 
class TestPlaceForm:
    def test_geocode_address_with_successful_geocoding(self, mocked_geocoder):

аналогичным образом можно создать еще одно приспособление для второго тестового типа

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