From f891fac3dc8d01b63828e0a02f60b58a093763de Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 15 Sep 2021 11:54:45 +0200 Subject: [PATCH] Refactor `tests.db.fake_data.factories` into a package --- setup.cfg | 2 + tests/db/fake_data/factories/__init__.py | 8 + tests/db/fake_data/factories/addresses.py | 40 ++++ tests/db/fake_data/factories/couriers.py | 24 +++ tests/db/fake_data/factories/customers.py | 16 ++ .../db/fake_data/factories/orders/__init__.py | 4 + .../orders/ad_hoc.py} | 190 ++---------------- .../fake_data/factories/orders/scheduled.py | 70 +++++++ tests/db/fake_data/factories/restaurants.py | 27 +++ tests/db/fake_data/factories/utils.py | 27 +++ 10 files changed, 236 insertions(+), 172 deletions(-) create mode 100644 tests/db/fake_data/factories/__init__.py create mode 100644 tests/db/fake_data/factories/addresses.py create mode 100644 tests/db/fake_data/factories/couriers.py create mode 100644 tests/db/fake_data/factories/customers.py create mode 100644 tests/db/fake_data/factories/orders/__init__.py rename tests/db/fake_data/{factories.py => factories/orders/ad_hoc.py} (54%) create mode 100644 tests/db/fake_data/factories/orders/scheduled.py create mode 100644 tests/db/fake_data/factories/restaurants.py create mode 100644 tests/db/fake_data/factories/utils.py diff --git a/setup.cfg b/setup.cfg index 56219e0..070e376 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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. diff --git a/tests/db/fake_data/factories/__init__.py b/tests/db/fake_data/factories/__init__.py new file mode 100644 index 0000000..215b2ae --- /dev/null +++ b/tests/db/fake_data/factories/__init__.py @@ -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 diff --git a/tests/db/fake_data/factories/addresses.py b/tests/db/fake_data/factories/addresses.py new file mode 100644 index 0000000..39ac266 --- /dev/null +++ b/tests/db/fake_data/factories/addresses.py @@ -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') diff --git a/tests/db/fake_data/factories/couriers.py b/tests/db/fake_data/factories/couriers.py new file mode 100644 index 0000000..1f1e751 --- /dev/null +++ b/tests/db/fake_data/factories/couriers.py @@ -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 diff --git a/tests/db/fake_data/factories/customers.py b/tests/db/fake_data/factories/customers.py new file mode 100644 index 0000000..55954e3 --- /dev/null +++ b/tests/db/fake_data/factories/customers.py @@ -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 diff --git a/tests/db/fake_data/factories/orders/__init__.py b/tests/db/fake_data/factories/orders/__init__.py new file mode 100644 index 0000000..407bbdb --- /dev/null +++ b/tests/db/fake_data/factories/orders/__init__.py @@ -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 diff --git a/tests/db/fake_data/factories.py b/tests/db/fake_data/factories/orders/ad_hoc.py similarity index 54% rename from tests/db/fake_data/factories.py rename to tests/db/fake_data/factories/orders/ad_hoc.py index 46f2ff3..29d6958 100644 --- a/tests/db/fake_data/factories.py +++ b/tests/db/fake_data/factories/orders/ad_hoc.py @@ -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), - ) diff --git a/tests/db/fake_data/factories/orders/scheduled.py b/tests/db/fake_data/factories/orders/scheduled.py new file mode 100644 index 0000000..4e71bdb --- /dev/null +++ b/tests/db/fake_data/factories/orders/scheduled.py @@ -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), + ) diff --git a/tests/db/fake_data/factories/restaurants.py b/tests/db/fake_data/factories/restaurants.py new file mode 100644 index 0000000..add3976 --- /dev/null +++ b/tests/db/fake_data/factories/restaurants.py @@ -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 diff --git a/tests/db/fake_data/factories/utils.py b/tests/db/fake_data/factories/utils.py new file mode 100644 index 0000000..953cddf --- /dev/null +++ b/tests/db/fake_data/factories/utils.py @@ -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)