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,
# Shadowing outer scopes occurs naturally with mocks.
WPS442,
# `utils` should be a valid module name.
WPS100,
# Test names may be longer than 40 characters.
WPS118,
# 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 random
import string
import factory
import faker
from factory import alchemy
from geopy import distance
from tests import config as test_config
from tests.db.fake_data.factories import utils
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):
"""Create instances of the `db.Order` model.
@ -145,7 +48,7 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
cancel_=True,
cancelled_at=factory.LazyAttribute(
lambda obj: obj.dispatch_at
+ _random_timespan(
+ utils.random_timespan(
max_seconds=(obj.pickup_at - obj.dispatch_at).total_seconds(),
),
),
@ -154,7 +57,7 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
cancel_=True,
cancelled_at=factory.LazyAttribute(
lambda obj: obj.pickup_at
+ _random_timespan(
+ utils.random_timespan(
max_seconds=(obj.delivery_at - obj.pickup_at).total_seconds(),
),
),
@ -170,7 +73,7 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
lambda: dt.datetime(
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
scheduled_delivery_at = None
@ -194,12 +97,13 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
# 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),
lambda obj: obj.placed_at
+ utils.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),
+ utils.random_timespan(min_seconds=30, max_seconds=150),
)
restaurant_confirmed_at_corrected = False
# Use the database defaults of the historic data.
@ -210,17 +114,18 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
# 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),
lambda obj: obj.placed_at
+ utils.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),
+ utils.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),
+ utils.random_timespan(min_seconds=15, max_seconds=45),
)
courier_accepted_at_corrected = False
# Sample a realistic utilization.
@ -230,16 +135,17 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
# 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),
+ utils.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),
+ utils.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),
lambda obj: obj.pickup_at
+ utils.random_timespan(min_seconds=60, max_seconds=180),
)
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`
reached_delivery_at = factory.LazyAttribute(
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(
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_not_confirmed = False
@ -316,63 +222,3 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory):
obj.delivery_at_corrected = None
obj.delivery_not_confirmed = 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)