Re-factor the ORM tests to use randomized fake data
- create `*Factory` classes with fakerboy and faker that generate randomized instances of the ORM models - add new pytest marker: "db" are the integration tests involving the database whereas "e2e" will be all other integration tests - streamline the docstrings in the ORM models
This commit is contained in:
parent
416a58f9dc
commit
78dba23d5d
19 changed files with 1092 additions and 721 deletions
14
tests/db/fake_data/__init__.py
Normal file
14
tests/db/fake_data/__init__.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"""Fixtures for testing the ORM layer with fake data."""
|
||||
|
||||
from tests.db.fake_data.fixture_makers import make_address # noqa:F401
|
||||
from tests.db.fake_data.fixture_makers import make_courier # noqa:F401
|
||||
from tests.db.fake_data.fixture_makers import make_customer # noqa:F401
|
||||
from tests.db.fake_data.fixture_makers import make_order # noqa:F401
|
||||
from tests.db.fake_data.fixture_makers import make_restaurant # noqa:F401
|
||||
from tests.db.fake_data.static_fixtures import address # noqa:F401
|
||||
from tests.db.fake_data.static_fixtures import city # noqa:F401
|
||||
from tests.db.fake_data.static_fixtures import city_data # noqa:F401
|
||||
from tests.db.fake_data.static_fixtures import courier # noqa:F401
|
||||
from tests.db.fake_data.static_fixtures import customer # noqa:F401
|
||||
from tests.db.fake_data.static_fixtures import order # noqa:F401
|
||||
from tests.db.fake_data.static_fixtures import restaurant # noqa:F401
|
||||
366
tests/db/fake_data/factories.py
Normal file
366
tests/db/fake_data/factories.py
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
"""Factories to create instances for the SQLAlchemy models."""
|
||||
|
||||
import datetime as dt
|
||||
import random
|
||||
import string
|
||||
|
||||
import factory
|
||||
import faker
|
||||
from factory import alchemy
|
||||
from geopy import distance
|
||||
|
||||
from urban_meal_delivery import db
|
||||
|
||||
|
||||
def _random_timespan( # noqa:WPS211
|
||||
*,
|
||||
min_hours=0,
|
||||
min_minutes=0,
|
||||
min_seconds=0,
|
||||
max_hours=0,
|
||||
max_minutes=0,
|
||||
max_seconds=0,
|
||||
):
|
||||
"""A randomized `timedelta` object between the specified arguments."""
|
||||
total_min_seconds = min_hours * 3600 + min_minutes * 60 + min_seconds
|
||||
total_max_seconds = max_hours * 3600 + max_minutes * 60 + max_seconds
|
||||
return dt.timedelta(seconds=random.randint(total_min_seconds, total_max_seconds))
|
||||
|
||||
|
||||
# The test day.
|
||||
_YEAR, _MONTH, _DAY = 2020, 1, 1
|
||||
|
||||
|
||||
def _early_in_the_morning():
|
||||
"""A randomized `datetime` object early in the morning."""
|
||||
return dt.datetime(_YEAR, _MONTH, _DAY, 3, 0) + _random_timespan(max_hours=2)
|
||||
|
||||
|
||||
class AddressFactory(alchemy.SQLAlchemyModelFactory):
|
||||
"""Create instances of the `db.Address` model."""
|
||||
|
||||
class Meta:
|
||||
model = db.Address
|
||||
sqlalchemy_get_or_create = ('id',)
|
||||
|
||||
id = factory.Sequence(lambda num: num) # noqa:WPS125
|
||||
created_at = factory.LazyFunction(_early_in_the_morning)
|
||||
|
||||
# When testing, all addresses are considered primary ones.
|
||||
# As non-primary addresses have no different behavior and
|
||||
# the property is only kept from the original dataset for
|
||||
# completeness sake, that is ok to do.
|
||||
_primary_id = factory.LazyAttribute(lambda obj: obj.id)
|
||||
|
||||
# Mimic a Google Maps Place ID with just random characters.
|
||||
place_id = factory.LazyFunction(
|
||||
lambda: ''.join(random.choice(string.ascii_lowercase) for _ in range(20)),
|
||||
)
|
||||
|
||||
# Place the addresses somewhere in downtown Paris.
|
||||
latitude = factory.Faker('coordinate', center=48.855, radius=0.01)
|
||||
longitude = factory.Faker('coordinate', center=2.34, radius=0.03)
|
||||
# city -> set by the `make_address` fixture as there is only one `city`
|
||||
city_name = 'Paris'
|
||||
zip_code = factory.LazyFunction(lambda: random.randint(75001, 75020))
|
||||
street = factory.Faker('street_address', locale='fr_FR')
|
||||
|
||||
|
||||
class CourierFactory(alchemy.SQLAlchemyModelFactory):
|
||||
"""Create instances of the `db.Courier` model."""
|
||||
|
||||
class Meta:
|
||||
model = db.Courier
|
||||
sqlalchemy_get_or_create = ('id',)
|
||||
|
||||
id = factory.Sequence(lambda num: num) # noqa:WPS125
|
||||
created_at = factory.LazyFunction(_early_in_the_morning)
|
||||
vehicle = 'bicycle'
|
||||
historic_speed = 7.89
|
||||
capacity = 100
|
||||
pay_per_hour = 750
|
||||
pay_per_order = 200
|
||||
|
||||
|
||||
class CustomerFactory(alchemy.SQLAlchemyModelFactory):
|
||||
"""Create instances of the `db.Customer` model."""
|
||||
|
||||
class Meta:
|
||||
model = db.Customer
|
||||
sqlalchemy_get_or_create = ('id',)
|
||||
|
||||
id = factory.Sequence(lambda num: num) # noqa:WPS125
|
||||
|
||||
|
||||
_restaurant_names = faker.Faker()
|
||||
|
||||
|
||||
class RestaurantFactory(alchemy.SQLAlchemyModelFactory):
|
||||
"""Create instances of the `db.Restaurant` model."""
|
||||
|
||||
class Meta:
|
||||
model = db.Restaurant
|
||||
sqlalchemy_get_or_create = ('id',)
|
||||
|
||||
id = factory.Sequence(lambda num: num) # noqa:WPS125
|
||||
created_at = factory.LazyFunction(_early_in_the_morning)
|
||||
name = factory.LazyFunction(
|
||||
lambda: f"{_restaurant_names.first_name()}'s Restaurant",
|
||||
)
|
||||
# address -> set by the `make_restaurant` fixture as there is only one `city`
|
||||
estimated_prep_duration = 1000
|
||||
|
||||
|
||||
class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
|
||||
"""Create instances of the `db.Order` model.
|
||||
|
||||
This factory creates ad-hoc `Order`s while the `ScheduledOrderFactory`
|
||||
below creates pre-orders. They are split into two classes mainly
|
||||
because the logic regarding how the timestamps are calculated from
|
||||
each other differs.
|
||||
|
||||
See the docstring in the contained `Params` class for
|
||||
flags to adapt how the `Order` is created.
|
||||
"""
|
||||
|
||||
# pylint:disable=too-many-instance-attributes
|
||||
|
||||
class Meta:
|
||||
model = db.Order
|
||||
sqlalchemy_get_or_create = ('id',)
|
||||
|
||||
class Params:
|
||||
"""Define flags that overwrite some attributes.
|
||||
|
||||
The `factory.Trait` objects in this class are executed after all
|
||||
the normal attributes in the `OrderFactory` classes were evaluated.
|
||||
|
||||
Flags:
|
||||
cancel_before_pickup
|
||||
cancel_after_pickup
|
||||
"""
|
||||
|
||||
# Timestamps after `cancelled_at` are discarded
|
||||
# by the `post_generation` hook at the end of the `OrderFactory`.
|
||||
cancel_ = factory.Trait( # noqa:WPS120 -> leading underscore does not work
|
||||
cancelled=True, cancelled_at_corrected=False,
|
||||
)
|
||||
cancel_before_pickup = factory.Trait(
|
||||
cancel_=True,
|
||||
cancelled_at=factory.LazyAttribute(
|
||||
lambda obj: obj.dispatch_at
|
||||
+ _random_timespan(
|
||||
max_seconds=(obj.pickup_at - obj.dispatch_at).total_seconds(),
|
||||
),
|
||||
),
|
||||
)
|
||||
cancel_after_pickup = factory.Trait(
|
||||
cancel_=True,
|
||||
cancelled_at=factory.LazyAttribute(
|
||||
lambda obj: obj.pickup_at
|
||||
+ _random_timespan(
|
||||
max_seconds=(obj.delivery_at - obj.pickup_at).total_seconds(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# Generic attributes
|
||||
id = factory.Sequence(lambda num: num) # noqa:WPS125
|
||||
# customer -> set by the `make_order` fixture for better control
|
||||
|
||||
# Attributes regarding the specialization of an `Order`: ad-hoc or scheduled.
|
||||
# Ad-hoc `Order`s are placed between 11.45 and 14.15.
|
||||
placed_at = factory.LazyFunction(
|
||||
lambda: dt.datetime(_YEAR, _MONTH, _DAY, 11, 45)
|
||||
+ _random_timespan(max_hours=2, max_minutes=30),
|
||||
)
|
||||
ad_hoc = True
|
||||
scheduled_delivery_at = None
|
||||
scheduled_delivery_at_corrected = None
|
||||
# Without statistical info, we assume an ad-hoc `Order` delivered after 45 minutes.
|
||||
first_estimated_delivery_at = factory.LazyAttribute(
|
||||
lambda obj: obj.placed_at + dt.timedelta(minutes=45),
|
||||
)
|
||||
|
||||
# Attributes regarding the cancellation of an `Order`.
|
||||
# May be overwritten with the `cancel_before_pickup` or `cancel_after_pickup` flags.
|
||||
cancelled = False
|
||||
cancelled_at = None
|
||||
cancelled_at_corrected = None
|
||||
|
||||
# Price-related attributes -> sample realistic prices
|
||||
sub_total = factory.LazyFunction(lambda: 100 * random.randint(15, 25))
|
||||
delivery_fee = 250
|
||||
total = factory.LazyAttribute(lambda obj: obj.sub_total + obj.delivery_fee)
|
||||
|
||||
# Restaurant-related attributes
|
||||
# restaurant -> set by the `make_order` fixture for better control
|
||||
restaurant_notified_at = factory.LazyAttribute(
|
||||
lambda obj: obj.placed_at + _random_timespan(min_seconds=30, max_seconds=90),
|
||||
)
|
||||
restaurant_notified_at_corrected = False
|
||||
restaurant_confirmed_at = factory.LazyAttribute(
|
||||
lambda obj: obj.restaurant_notified_at
|
||||
+ _random_timespan(min_seconds=30, max_seconds=150),
|
||||
)
|
||||
restaurant_confirmed_at_corrected = False
|
||||
# Use the database defaults of the historic data.
|
||||
estimated_prep_duration = 900
|
||||
estimated_prep_duration_corrected = False
|
||||
estimated_prep_buffer = 480
|
||||
|
||||
# Dispatch-related columns
|
||||
# courier -> set by the `make_order` fixture for better control
|
||||
dispatch_at = factory.LazyAttribute(
|
||||
lambda obj: obj.placed_at + _random_timespan(min_seconds=600, max_seconds=1080),
|
||||
)
|
||||
dispatch_at_corrected = False
|
||||
courier_notified_at = factory.LazyAttribute(
|
||||
lambda obj: obj.dispatch_at
|
||||
+ _random_timespan(min_seconds=100, max_seconds=140),
|
||||
)
|
||||
courier_notified_at_corrected = False
|
||||
courier_accepted_at = factory.LazyAttribute(
|
||||
lambda obj: obj.courier_notified_at
|
||||
+ _random_timespan(min_seconds=15, max_seconds=45),
|
||||
)
|
||||
courier_accepted_at_corrected = False
|
||||
# Sample a realistic utilization.
|
||||
utilization = factory.LazyFunction(lambda: random.choice([50, 60, 70, 80, 90, 100]))
|
||||
|
||||
# Pickup-related attributes
|
||||
# pickup_address -> aligned with `restaurant.address` by the `make_order` fixture
|
||||
reached_pickup_at = factory.LazyAttribute(
|
||||
lambda obj: obj.courier_accepted_at
|
||||
+ _random_timespan(min_seconds=300, max_seconds=600),
|
||||
)
|
||||
pickup_at = factory.LazyAttribute(
|
||||
lambda obj: obj.reached_pickup_at
|
||||
+ _random_timespan(min_seconds=120, max_seconds=600),
|
||||
)
|
||||
pickup_at_corrected = False
|
||||
pickup_not_confirmed = False
|
||||
left_pickup_at = factory.LazyAttribute(
|
||||
lambda obj: obj.pickup_at + _random_timespan(min_seconds=60, max_seconds=180),
|
||||
)
|
||||
left_pickup_at_corrected = False
|
||||
|
||||
# Delivery-related attributes
|
||||
# delivery_address -> set by the `make_order` fixture as there is only one `city`
|
||||
reached_delivery_at = factory.LazyAttribute(
|
||||
lambda obj: obj.left_pickup_at
|
||||
+ _random_timespan(min_seconds=240, max_seconds=480),
|
||||
)
|
||||
delivery_at = factory.LazyAttribute(
|
||||
lambda obj: obj.reached_delivery_at
|
||||
+ _random_timespan(min_seconds=240, max_seconds=660),
|
||||
)
|
||||
delivery_at_corrected = False
|
||||
delivery_not_confirmed = False
|
||||
_courier_waited_at_delivery = factory.LazyAttribute(
|
||||
lambda obj: False if obj.delivery_at else None,
|
||||
)
|
||||
|
||||
# Statistical attributes -> calculate realistic stats
|
||||
logged_delivery_distance = factory.LazyAttribute(
|
||||
lambda obj: distance.great_circle( # noqa:WPS317
|
||||
(obj.pickup_address.latitude, obj.pickup_address.longitude),
|
||||
(obj.delivery_address.latitude, obj.delivery_address.longitude),
|
||||
).meters,
|
||||
)
|
||||
logged_avg_speed = factory.LazyAttribute( # noqa:ECE001
|
||||
lambda obj: round(
|
||||
(
|
||||
obj.logged_avg_speed_distance
|
||||
/ (obj.delivery_at - obj.pickup_at).total_seconds()
|
||||
),
|
||||
2,
|
||||
),
|
||||
)
|
||||
logged_avg_speed_distance = factory.LazyAttribute(
|
||||
lambda obj: 0.95 * obj.logged_delivery_distance,
|
||||
)
|
||||
|
||||
@factory.post_generation
|
||||
def post( # noqa:C901,WPS23 pylint:disable=unused-argument
|
||||
obj, create, extracted, **kwargs, # noqa:B902,N805
|
||||
):
|
||||
"""Discard timestamps that occur after cancellation."""
|
||||
if obj.cancelled:
|
||||
if obj.cancelled_at <= obj.restaurant_notified_at:
|
||||
obj.restaurant_notified_at = None
|
||||
obj.restaurant_notified_at_corrected = None
|
||||
if obj.cancelled_at <= obj.restaurant_confirmed_at:
|
||||
obj.restaurant_confirmed_at = None
|
||||
obj.restaurant_confirmed_at_corrected = None
|
||||
if obj.cancelled_at <= obj.dispatch_at:
|
||||
obj.dispatch_at = None
|
||||
obj.dispatch_at_corrected = None
|
||||
if obj.cancelled_at <= obj.courier_notified_at:
|
||||
obj.courier_notified_at = None
|
||||
obj.courier_notified_at_corrected = None
|
||||
if obj.cancelled_at <= obj.courier_accepted_at:
|
||||
obj.courier_accepted_at = None
|
||||
obj.courier_accepted_at_corrected = None
|
||||
if obj.cancelled_at <= obj.reached_pickup_at:
|
||||
obj.reached_pickup_at = None
|
||||
if obj.cancelled_at <= obj.pickup_at:
|
||||
obj.pickup_at = None
|
||||
obj.pickup_at_corrected = None
|
||||
obj.pickup_not_confirmed = None
|
||||
if obj.cancelled_at <= obj.left_pickup_at:
|
||||
obj.left_pickup_at = None
|
||||
obj.left_pickup_at_corrected = None
|
||||
if obj.cancelled_at <= obj.reached_delivery_at:
|
||||
obj.reached_delivery_at = None
|
||||
if obj.cancelled_at <= obj.delivery_at:
|
||||
obj.delivery_at = None
|
||||
obj.delivery_at_corrected = None
|
||||
obj.delivery_not_confirmed = None
|
||||
obj._courier_waited_at_delivery = None # noqa:WPS437
|
||||
|
||||
|
||||
class ScheduledOrderFactory(AdHocOrderFactory):
|
||||
"""Create instances of the `db.Order` model.
|
||||
|
||||
This class takes care of the various timestamps for pre-orders.
|
||||
|
||||
Pre-orders are placed long before the test day's lunch time starts.
|
||||
All timestamps are relative to either `.dispatch_at` or `.restaurant_notified_at`
|
||||
and calculated backwards from `.scheduled_delivery_at`.
|
||||
"""
|
||||
|
||||
# Attributes regarding the specialization of an `Order`: ad-hoc or scheduled.
|
||||
placed_at = factory.LazyFunction(_early_in_the_morning)
|
||||
ad_hoc = False
|
||||
# Discrete `datetime` objects in the "core" lunch time are enough.
|
||||
scheduled_delivery_at = factory.LazyFunction(
|
||||
lambda: random.choice(
|
||||
[
|
||||
dt.datetime(_YEAR, _MONTH, _DAY, 12, 0),
|
||||
dt.datetime(_YEAR, _MONTH, _DAY, 12, 15),
|
||||
dt.datetime(_YEAR, _MONTH, _DAY, 12, 30),
|
||||
dt.datetime(_YEAR, _MONTH, _DAY, 12, 45),
|
||||
dt.datetime(_YEAR, _MONTH, _DAY, 13, 0),
|
||||
dt.datetime(_YEAR, _MONTH, _DAY, 13, 15),
|
||||
dt.datetime(_YEAR, _MONTH, _DAY, 13, 30),
|
||||
],
|
||||
),
|
||||
)
|
||||
scheduled_delivery_at_corrected = False
|
||||
# Assume the `Order` is on time.
|
||||
first_estimated_delivery_at = factory.LazyAttribute(
|
||||
lambda obj: obj.scheduled_delivery_at,
|
||||
)
|
||||
|
||||
# Restaurant-related attributes
|
||||
restaurant_notified_at = factory.LazyAttribute(
|
||||
lambda obj: obj.scheduled_delivery_at
|
||||
- _random_timespan(min_minutes=45, max_minutes=50),
|
||||
)
|
||||
|
||||
# Dispatch-related attributes
|
||||
dispatch_at = factory.LazyAttribute(
|
||||
lambda obj: obj.scheduled_delivery_at
|
||||
- _random_timespan(min_minutes=40, max_minutes=45),
|
||||
)
|
||||
105
tests/db/fake_data/fixture_makers.py
Normal file
105
tests/db/fake_data/fixture_makers.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"""Fixture factories for testing the ORM layer with fake data."""
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.db.fake_data import factories
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_address(city):
|
||||
"""Replaces `AddressFactory.build()`: Create an `Address` in the `city`."""
|
||||
# Reset the identifiers before every test.
|
||||
factories.AddressFactory.reset_sequence(1)
|
||||
|
||||
def func(**kwargs):
|
||||
"""Create an `Address` object in the `city`."""
|
||||
return factories.AddressFactory.build(city=city, **kwargs)
|
||||
|
||||
return func
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_courier():
|
||||
"""Replaces `CourierFactory.build()`: Create a `Courier`."""
|
||||
# Reset the identifiers before every test.
|
||||
factories.CourierFactory.reset_sequence(1)
|
||||
|
||||
def func(**kwargs):
|
||||
"""Create a new `Courier` object."""
|
||||
return factories.CourierFactory.build(**kwargs)
|
||||
|
||||
return func
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_customer():
|
||||
"""Replaces `CustomerFactory.build()`: Create a `Customer`."""
|
||||
# Reset the identifiers before every test.
|
||||
factories.CustomerFactory.reset_sequence(1)
|
||||
|
||||
def func(**kwargs):
|
||||
"""Create a new `Customer` object."""
|
||||
return factories.CustomerFactory.build(**kwargs)
|
||||
|
||||
return func
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_restaurant(make_address):
|
||||
"""Replaces `RestaurantFactory.build()`: Create a `Restaurant`."""
|
||||
# Reset the identifiers before every test.
|
||||
factories.RestaurantFactory.reset_sequence(1)
|
||||
|
||||
def func(address=None, **kwargs):
|
||||
"""Create a new `Restaurant` object.
|
||||
|
||||
If no `address` is provided, a new `Address` is created.
|
||||
"""
|
||||
if address is None:
|
||||
address = make_address()
|
||||
|
||||
return factories.RestaurantFactory.build(address=address, **kwargs)
|
||||
|
||||
return func
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_order(make_address, make_courier, make_customer, make_restaurant):
|
||||
"""Replaces `OrderFactory.build()`: Create a `Order`."""
|
||||
# Reset the identifiers before every test.
|
||||
factories.AdHocOrderFactory.reset_sequence(1)
|
||||
|
||||
def func(scheduled=False, restaurant=None, courier=None, **kwargs):
|
||||
"""Create a new `Order` object.
|
||||
|
||||
Each `Order` is made by a new `Customer` with a unique `Address` for delivery.
|
||||
|
||||
Args:
|
||||
scheduled: if an `Order` is a pre-order
|
||||
restaurant: who receives the `Order`; defaults to a new `Restaurant`
|
||||
courier: who delivered the `Order`; defaults to a new `Courier`
|
||||
kwargs: additional keyword arguments forwarded to the `OrderFactory`
|
||||
|
||||
Returns:
|
||||
order
|
||||
"""
|
||||
if scheduled:
|
||||
factory_cls = factories.ScheduledOrderFactory
|
||||
else:
|
||||
factory_cls = factories.AdHocOrderFactory
|
||||
|
||||
if restaurant is None:
|
||||
restaurant = make_restaurant()
|
||||
if courier is None:
|
||||
courier = make_courier()
|
||||
|
||||
return factory_cls.build(
|
||||
customer=make_customer(), # assume a unique `Customer` per order
|
||||
restaurant=restaurant,
|
||||
courier=courier,
|
||||
pickup_address=restaurant.address, # no `Address` history
|
||||
delivery_address=make_address(), # unique `Customer` => new `Address`
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return func
|
||||
58
tests/db/fake_data/static_fixtures.py
Normal file
58
tests/db/fake_data/static_fixtures.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"""Fake data for testing the ORM layer."""
|
||||
|
||||
import pytest
|
||||
|
||||
from urban_meal_delivery import db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def city_data():
|
||||
"""The data for the one and only `City` object as a `dict`."""
|
||||
return {
|
||||
'id': 1,
|
||||
'name': 'Paris',
|
||||
'kml': "<?xml version='1.0' encoding='UTF-8'?> ...",
|
||||
'_center_latitude': 48.856614,
|
||||
'_center_longitude': 2.3522219,
|
||||
'_northeast_latitude': 48.9021449,
|
||||
'_northeast_longitude': 2.4699208,
|
||||
'_southwest_latitude': 48.815573,
|
||||
'_southwest_longitude': 2.225193,
|
||||
'initial_zoom': 12,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def city(city_data):
|
||||
"""The one and only `City` object."""
|
||||
return db.City(**city_data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def address(make_address):
|
||||
"""An `Address` object in the `city`."""
|
||||
return make_address()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def courier(make_courier):
|
||||
"""A `Courier` object."""
|
||||
return make_courier()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def customer(make_customer):
|
||||
"""A `Customer` object."""
|
||||
return make_customer()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def restaurant(address, make_restaurant):
|
||||
"""A `Restaurant` object located at the `address`."""
|
||||
return make_restaurant(address=address)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def order(make_order, restaurant):
|
||||
"""An `Order` object for the `restaurant`."""
|
||||
return make_order(restaurant=restaurant)
|
||||
Loading…
Add table
Add a link
Reference in a new issue