Refactor tests.db.fake_data.factories into a package

This commit is contained in:
Alexander Hess 2021-09-15 11:54:45 +02:00
parent 7e23033d84
commit f891fac3dc
Signed by: alexander
GPG key ID: 344EA5AB10D868E0
10 changed files with 236 additions and 172 deletions

View file

@ -167,6 +167,8 @@ per-file-ignores =
S311, S311,
# Shadowing outer scopes occurs naturally with mocks. # Shadowing outer scopes occurs naturally with mocks.
WPS442, WPS442,
# `utils` should be a valid module name.
WPS100,
# Test names may be longer than 40 characters. # Test names may be longer than 40 characters.
WPS118, WPS118,
# Modules may have many test cases. # Modules may have many test cases.

View file

@ -0,0 +1,8 @@
"""Factories to create instances for the SQLAlchemy models."""
from tests.db.fake_data.factories.addresses import AddressFactory
from tests.db.fake_data.factories.couriers import CourierFactory
from tests.db.fake_data.factories.customers import CustomerFactory
from tests.db.fake_data.factories.orders import AdHocOrderFactory
from tests.db.fake_data.factories.orders import ScheduledOrderFactory
from tests.db.fake_data.factories.restaurants import RestaurantFactory

View file

@ -0,0 +1,40 @@
"""Factory to create `Address` instances."""
import random
import string
import factory
from factory import alchemy
from tests.db.fake_data.factories import utils
from urban_meal_delivery import db
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(utils.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')

View file

@ -0,0 +1,24 @@
"""Factory to create `Courier` instances."""
import factory
from factory import alchemy
from tests.db.fake_data.factories import utils
from urban_meal_delivery import db
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(utils.early_in_the_morning)
vehicle = 'bicycle'
historic_speed = 7.89
capacity = 100
pay_per_hour = 750
pay_per_order = 200

View file

@ -0,0 +1,16 @@
"""Factory to create `Customers` instances."""
import factory
from factory import alchemy
from urban_meal_delivery import db
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

View file

@ -0,0 +1,4 @@
"""Factory to create `Order` instances."""
from tests.db.fake_data.factories.orders.ad_hoc import AdHocOrderFactory
from tests.db.fake_data.factories.orders.scheduled import ScheduledOrderFactory

View file

@ -1,114 +1,17 @@
"""Factories to create instances for the SQLAlchemy models.""" """Factory to create ad-hoc `Order` instances."""
import datetime as dt import datetime as dt
import random import random
import string
import factory import factory
import faker
from factory import alchemy from factory import alchemy
from geopy import distance from geopy import distance
from tests import config as test_config from tests import config as test_config
from tests.db.fake_data.factories import utils
from urban_meal_delivery import db 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))
def _early_in_the_morning():
"""A randomized `datetime` object early in the morning."""
early = dt.datetime(test_config.YEAR, test_config.MONTH, test_config.DAY, 3, 0)
return early + _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): class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
"""Create instances of the `db.Order` model. """Create instances of the `db.Order` model.
@ -145,7 +48,7 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
cancel_=True, cancel_=True,
cancelled_at=factory.LazyAttribute( cancelled_at=factory.LazyAttribute(
lambda obj: obj.dispatch_at lambda obj: obj.dispatch_at
+ _random_timespan( + utils.random_timespan(
max_seconds=(obj.pickup_at - obj.dispatch_at).total_seconds(), max_seconds=(obj.pickup_at - obj.dispatch_at).total_seconds(),
), ),
), ),
@ -154,7 +57,7 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
cancel_=True, cancel_=True,
cancelled_at=factory.LazyAttribute( cancelled_at=factory.LazyAttribute(
lambda obj: obj.pickup_at lambda obj: obj.pickup_at
+ _random_timespan( + utils.random_timespan(
max_seconds=(obj.delivery_at - obj.pickup_at).total_seconds(), max_seconds=(obj.delivery_at - obj.pickup_at).total_seconds(),
), ),
), ),
@ -170,7 +73,7 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
lambda: dt.datetime( lambda: dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.DAY, 11, 45, test_config.YEAR, test_config.MONTH, test_config.DAY, 11, 45,
) )
+ _random_timespan(max_hours=2, max_minutes=30), + utils.random_timespan(max_hours=2, max_minutes=30),
) )
ad_hoc = True ad_hoc = True
scheduled_delivery_at = None scheduled_delivery_at = None
@ -194,12 +97,13 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
# Restaurant-related attributes # Restaurant-related attributes
# restaurant -> set by the `make_order` fixture for better control # restaurant -> set by the `make_order` fixture for better control
restaurant_notified_at = factory.LazyAttribute( restaurant_notified_at = factory.LazyAttribute(
lambda obj: obj.placed_at + _random_timespan(min_seconds=30, max_seconds=90), lambda obj: obj.placed_at
+ utils.random_timespan(min_seconds=30, max_seconds=90),
) )
restaurant_notified_at_corrected = False restaurant_notified_at_corrected = False
restaurant_confirmed_at = factory.LazyAttribute( restaurant_confirmed_at = factory.LazyAttribute(
lambda obj: obj.restaurant_notified_at lambda obj: obj.restaurant_notified_at
+ _random_timespan(min_seconds=30, max_seconds=150), + utils.random_timespan(min_seconds=30, max_seconds=150),
) )
restaurant_confirmed_at_corrected = False restaurant_confirmed_at_corrected = False
# Use the database defaults of the historic data. # Use the database defaults of the historic data.
@ -210,17 +114,18 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
# Dispatch-related columns # Dispatch-related columns
# courier -> set by the `make_order` fixture for better control # courier -> set by the `make_order` fixture for better control
dispatch_at = factory.LazyAttribute( dispatch_at = factory.LazyAttribute(
lambda obj: obj.placed_at + _random_timespan(min_seconds=600, max_seconds=1080), lambda obj: obj.placed_at
+ utils.random_timespan(min_seconds=600, max_seconds=1080),
) )
dispatch_at_corrected = False dispatch_at_corrected = False
courier_notified_at = factory.LazyAttribute( courier_notified_at = factory.LazyAttribute(
lambda obj: obj.dispatch_at lambda obj: obj.dispatch_at
+ _random_timespan(min_seconds=100, max_seconds=140), + utils.random_timespan(min_seconds=100, max_seconds=140),
) )
courier_notified_at_corrected = False courier_notified_at_corrected = False
courier_accepted_at = factory.LazyAttribute( courier_accepted_at = factory.LazyAttribute(
lambda obj: obj.courier_notified_at lambda obj: obj.courier_notified_at
+ _random_timespan(min_seconds=15, max_seconds=45), + utils.random_timespan(min_seconds=15, max_seconds=45),
) )
courier_accepted_at_corrected = False courier_accepted_at_corrected = False
# Sample a realistic utilization. # Sample a realistic utilization.
@ -230,16 +135,17 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
# pickup_address -> aligned with `restaurant.address` by the `make_order` fixture # pickup_address -> aligned with `restaurant.address` by the `make_order` fixture
reached_pickup_at = factory.LazyAttribute( reached_pickup_at = factory.LazyAttribute(
lambda obj: obj.courier_accepted_at lambda obj: obj.courier_accepted_at
+ _random_timespan(min_seconds=300, max_seconds=600), + utils.random_timespan(min_seconds=300, max_seconds=600),
) )
pickup_at = factory.LazyAttribute( pickup_at = factory.LazyAttribute(
lambda obj: obj.reached_pickup_at lambda obj: obj.reached_pickup_at
+ _random_timespan(min_seconds=120, max_seconds=600), + utils.random_timespan(min_seconds=120, max_seconds=600),
) )
pickup_at_corrected = False pickup_at_corrected = False
pickup_not_confirmed = False pickup_not_confirmed = False
left_pickup_at = factory.LazyAttribute( left_pickup_at = factory.LazyAttribute(
lambda obj: obj.pickup_at + _random_timespan(min_seconds=60, max_seconds=180), lambda obj: obj.pickup_at
+ utils.random_timespan(min_seconds=60, max_seconds=180),
) )
left_pickup_at_corrected = False left_pickup_at_corrected = False
@ -247,11 +153,11 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
# delivery_address -> set by the `make_order` fixture as there is only one `city` # delivery_address -> set by the `make_order` fixture as there is only one `city`
reached_delivery_at = factory.LazyAttribute( reached_delivery_at = factory.LazyAttribute(
lambda obj: obj.left_pickup_at lambda obj: obj.left_pickup_at
+ _random_timespan(min_seconds=240, max_seconds=480), + utils.random_timespan(min_seconds=240, max_seconds=480),
) )
delivery_at = factory.LazyAttribute( delivery_at = factory.LazyAttribute(
lambda obj: obj.reached_delivery_at lambda obj: obj.reached_delivery_at
+ _random_timespan(min_seconds=240, max_seconds=660), + utils.random_timespan(min_seconds=240, max_seconds=660),
) )
delivery_at_corrected = False delivery_at_corrected = False
delivery_not_confirmed = False delivery_not_confirmed = False
@ -316,63 +222,3 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
obj.delivery_at_corrected = None obj.delivery_at_corrected = None
obj.delivery_not_confirmed = None obj.delivery_not_confirmed = None
obj._courier_waited_at_delivery = None obj._courier_waited_at_delivery = None
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(
test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 0,
),
dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 15,
),
dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 30,
),
dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 45,
),
dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.DAY, 13, 0,
),
dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.DAY, 13, 15,
),
dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.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),
)

View file

@ -0,0 +1,70 @@
"""Factory to create scheduled `Order` instances."""
import datetime as dt
import random
import factory
from tests import config as test_config
from tests.db.fake_data.factories import utils
from tests.db.fake_data.factories.orders import ad_hoc
class ScheduledOrderFactory(ad_hoc.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(utils.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(
test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 0,
),
dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 15,
),
dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 30,
),
dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 45,
),
dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.DAY, 13, 0,
),
dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.DAY, 13, 15,
),
dt.datetime(
test_config.YEAR, test_config.MONTH, test_config.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
- utils.random_timespan(min_minutes=45, max_minutes=50),
)
# Dispatch-related attributes
dispatch_at = factory.LazyAttribute(
lambda obj: obj.scheduled_delivery_at
- utils.random_timespan(min_minutes=40, max_minutes=45),
)

View file

@ -0,0 +1,27 @@
"""Factory to create `Restaurant` instances."""
import factory
import faker
from factory import alchemy
from tests.db.fake_data.factories import utils
from urban_meal_delivery import db
_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(utils.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

View file

@ -0,0 +1,27 @@
"""Utilities used in all `*Factory` classes."""
import datetime as dt
import random
from tests import config as test_config
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))
def early_in_the_morning():
"""A randomized `datetime` object early in the morning."""
early = dt.datetime(test_config.YEAR, test_config.MONTH, test_config.DAY, 3, 0)
return early + random_timespan(max_hours=2)