Django:在模型的 save 方法中模拟外部 api 调用

问题描述 投票:0回答:1

我想在两种模式下使用 pytest 测试模型:

  1. 无需调用save方法中使用的外部API
  2. 通过在 API 离线时生成错误,以便我可以测试我创建的验证

这是我的代码:

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 的情况下模拟 save 方法

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

这里是覆盖地图框不可用验证步骤的代码

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),但我不明白如何做到这一点。有人可以帮忙吗?

django mocking pytest mapbox
1个回答
0
投票

我认为你可以使用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):

同样,您可以为第二个测试类型创建另一个夹具

© www.soinside.com 2019 - 2024. All rights reserved.