Django: Подражание вызову внешнего api в методе сохранения модели
Я хочу протестировать модельную форму с помощью pytest в двух режимах:
- без необходимости вызова внешнего API, используемого в методе сохранения
- генерируя ошибку, когда 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
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):
аналогичным образом можно создать еще одно приспособление для второго тестового типа