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:
Alexander Hess 2020-12-29 14:37:37 +01:00
parent 416a58f9dc
commit 78dba23d5d
Signed by: alexander
GPG key ID: 344EA5AB10D868E0
19 changed files with 1092 additions and 721 deletions

View file

@ -222,6 +222,9 @@ def test(session):
session.run('poetry', 'install', '--no-dev', external=True)
_install_packages(
session,
'Faker',
'factory-boy',
'geopy',
'packaging',
'pytest',
'pytest-cov',
@ -242,8 +245,8 @@ def test(session):
'--cov-fail-under=100',
'--cov-report=term-missing:skip-covered',
'--randomly-seed=4287',
'-k',
'not e2e',
'-m',
'not (db or e2e)',
PYTEST_LOCATION,
)
session.run('pytest', '--version')

View file

@ -89,6 +89,8 @@ extend-ignore =
# Comply with black's style.
# Source: https://github.com/psf/black/blob/master/docs/compatible_configs.md#flake8
E203, W503, WPS348,
# Allow underscores in numbers.
WPS303,
# f-strings are ok.
WPS305,
# Classes should not have to specify a base class.
@ -139,14 +141,24 @@ per-file-ignores =
tests/*.py:
# Type annotations are not strictly enforced.
ANN0, ANN2,
# The `Meta` class inside the factory_boy models do not need a docstring.
D106,
# `assert` statements are ok in the test suite.
S101,
# The `random` module is not used for cryptography.
S311,
# Shadowing outer scopes occurs naturally with mocks.
WPS442,
# Modules may have many test cases.
WPS202,WPS204,WPS214,
# Do not check for Jones complexity in the test suite.
WPS221,
# No overuse of string constants (e.g., '__version__').
WPS226,
# We do not care about the number of "# noqa"s in the test suite.
WPS402,
# Allow closures.
WPS430,
# Numbers are normal in test cases as expected results.
WPS432,
@ -166,6 +178,7 @@ show-source = true
# wemake-python-styleguide's settings
# ===================================
allowed-domain-names =
data,
obj,
param,
result,
@ -274,4 +287,5 @@ console_output_style = count
env =
TESTING=true
markers =
e2e: integration tests, incl., for example, tests touching the database
db: tests touching the database
e2e: non-db integration tests

View file

@ -1,4 +1,4 @@
"""Provide the ORM's Address model."""
"""Provide the ORM's `Address` model."""
import sqlalchemy as sa
from sqlalchemy import orm
@ -9,7 +9,7 @@ from urban_meal_delivery.db import meta
class Address(meta.Base):
"""An Address of a Customer or a Restaurant on the UDP."""
"""An address of a `Customer` or a `Restaurant` on the UDP."""
__tablename__ = 'addresses'
@ -72,11 +72,11 @@ class Address(meta.Base):
@hybrid.hybrid_property
def is_primary(self) -> bool:
"""If an Address object is the earliest one entered at its location.
"""If an `Address` object is the earliest one entered at its location.
Street addresses may have been entered several times with different
versions/spellings of the street name and/or different floors.
`is_primary` indicates the first in a group of addresses.
`.is_primary` indicates the first in a group of `Address` objects.
"""
return self.id == self._primary_id

View file

@ -1,4 +1,4 @@
"""Provide the ORM's City model."""
"""Provide the ORM's `City` model."""
from typing import Dict
@ -10,7 +10,7 @@ from urban_meal_delivery.db import meta
class City(meta.Base):
"""A City where the UDP operates in."""
"""A city where the UDP operates in."""
__tablename__ = 'cities'

View file

@ -1,4 +1,4 @@
"""Provide the ORM's Courier model."""
"""Provide the ORM's `Courier` model."""
import sqlalchemy as sa
from sqlalchemy import orm
@ -8,7 +8,7 @@ from urban_meal_delivery.db import meta
class Courier(meta.Base):
"""A Courier working for the UDP."""
"""A courier working for the UDP."""
__tablename__ = 'couriers'

View file

@ -1,4 +1,4 @@
"""Provide the ORM's Customer model."""
"""Provide the ORM's `Customer` model."""
import sqlalchemy as sa
from sqlalchemy import orm
@ -7,7 +7,7 @@ from urban_meal_delivery.db import meta
class Customer(meta.Base):
"""A Customer of the UDP."""
"""A customer of the UDP."""
__tablename__ = 'customers'

View file

@ -1,4 +1,4 @@
"""Provide the ORM's Order model."""
"""Provide the ORM's `Order` model."""
import datetime
@ -10,7 +10,7 @@ from urban_meal_delivery.db import meta
class Order(meta.Base): # noqa:WPS214
"""An Order by a Customer of the UDP."""
"""An order by a `Customer` of the UDP."""
__tablename__ = 'orders'
@ -325,12 +325,12 @@ class Order(meta.Base): # noqa:WPS214
@property
def scheduled(self) -> bool:
"""Inverse of Order.ad_hoc."""
"""Inverse of `.ad_hoc`."""
return not self.ad_hoc
@property
def completed(self) -> bool:
"""Inverse of Order.cancelled."""
"""Inverse of `.cancelled`."""
return not self.cancelled
@property
@ -353,9 +353,9 @@ class Order(meta.Base): # noqa:WPS214
@property
def time_to_accept(self) -> datetime.timedelta:
"""Time until a courier accepted an order.
"""Time until the `.courier` accepted the order.
This adds the time it took the UDP to notify a courier.
This measures the time it took the UDP to notify the `.courier` after dispatch.
"""
if not self.dispatch_at:
raise RuntimeError('dispatch_at is not set')
@ -365,9 +365,9 @@ class Order(meta.Base): # noqa:WPS214
@property
def time_to_react(self) -> datetime.timedelta:
"""Time a courier took to accept an order.
"""Time the `.courier` took to accept an order.
This time is a subset of Order.time_to_accept.
A subset of `.time_to_accept`.
"""
if not self.courier_notified_at:
raise RuntimeError('courier_notified_at is not set')
@ -377,7 +377,7 @@ class Order(meta.Base): # noqa:WPS214
@property
def time_to_pickup(self) -> datetime.timedelta:
"""Time from a courier's acceptance to arrival at the pickup location."""
"""Time from the `.courier`'s acceptance to arrival at `.pickup_address`."""
if not self.courier_accepted_at:
raise RuntimeError('courier_accepted_at is not set')
if not self.reached_pickup_at:
@ -386,7 +386,7 @@ class Order(meta.Base): # noqa:WPS214
@property
def time_at_pickup(self) -> datetime.timedelta:
"""Time a courier stayed at the pickup location."""
"""Time the `.courier` stayed at the `.pickup_address`."""
if not self.reached_pickup_at:
raise RuntimeError('reached_pickup_at is not set')
if not self.pickup_at:
@ -405,13 +405,13 @@ class Order(meta.Base): # noqa:WPS214
@property
def courier_early(self) -> datetime.timedelta:
"""Time by which a courier is early for pickup.
"""Time by which the `.courier` is early for pickup.
Measured relative to Order.scheduled_pickup_at.
Measured relative to `.scheduled_pickup_at`.
0 if the courier is on time or late.
`datetime.timedelta(seconds=0)` if the `.courier` is on time or late.
Goes together with Order.courier_late.
Goes together with `.courier_late`.
"""
return max(
datetime.timedelta(), self.scheduled_pickup_at - self.reached_pickup_at,
@ -419,13 +419,13 @@ class Order(meta.Base): # noqa:WPS214
@property
def courier_late(self) -> datetime.timedelta:
"""Time by which a courier is late for pickup.
"""Time by which the `.courier` is late for pickup.
Measured relative to Order.scheduled_pickup_at.
Measured relative to `.scheduled_pickup_at`.
0 if the courier is on time or early.
`datetime.timedelta(seconds=0)` if the `.courier` is on time or early.
Goes together with Order.courier_early.
Goes together with `.courier_early`.
"""
return max(
datetime.timedelta(), self.reached_pickup_at - self.scheduled_pickup_at,
@ -433,31 +433,31 @@ class Order(meta.Base): # noqa:WPS214
@property
def restaurant_early(self) -> datetime.timedelta:
"""Time by which a restaurant is early for pickup.
"""Time by which the `.restaurant` is early for pickup.
Measured relative to Order.scheduled_pickup_at.
Measured relative to `.scheduled_pickup_at`.
0 if the restaurant is on time or late.
`datetime.timedelta(seconds=0)` if the `.restaurant` is on time or late.
Goes together with Order.restaurant_late.
Goes together with `.restaurant_late`.
"""
return max(datetime.timedelta(), self.scheduled_pickup_at - self.pickup_at)
@property
def restaurant_late(self) -> datetime.timedelta:
"""Time by which a restaurant is late for pickup.
"""Time by which the `.restaurant` is late for pickup.
Measured relative to Order.scheduled_pickup_at.
Measured relative to `.scheduled_pickup_at`.
0 if the restaurant is on time or early.
`datetime.timedelta(seconds=0)` if the `.restaurant` is on time or early.
Goes together with Order.restaurant_early.
Goes together with `.restaurant_early`.
"""
return max(datetime.timedelta(), self.pickup_at - self.scheduled_pickup_at)
@property
def time_to_delivery(self) -> datetime.timedelta:
"""Time a courier took from pickup to delivery location."""
"""Time the `.courier` took from `.pickup_address` to `.delivery_address`."""
if not self.pickup_at:
raise RuntimeError('pickup_at is not set')
if not self.reached_delivery_at:
@ -466,7 +466,7 @@ class Order(meta.Base): # noqa:WPS214
@property
def time_at_delivery(self) -> datetime.timedelta:
"""Time a courier stayed at the delivery location."""
"""Time the `.courier` stayed at the `.delivery_address`."""
if not self.reached_delivery_at:
raise RuntimeError('reached_delivery_at is not set')
if not self.delivery_at:
@ -475,20 +475,20 @@ class Order(meta.Base): # noqa:WPS214
@property
def courier_waited_at_delivery(self) -> datetime.timedelta:
"""Time a courier waited at the delivery location."""
"""Time the `.courier` waited at the `.delivery_address`."""
if self._courier_waited_at_delivery:
return self.time_at_delivery
return datetime.timedelta()
@property
def delivery_early(self) -> datetime.timedelta:
"""Time by which a scheduled order was early.
"""Time by which a `.scheduled` order was early.
Measured relative to Order.scheduled_delivery_at.
Measured relative to `.scheduled_delivery_at`.
0 if the delivery is on time or late.
`datetime.timedelta(seconds=0)` if the delivery is on time or late.
Goes together with Order.delivery_late.
Goes together with `.delivery_late`.
"""
if not self.scheduled:
raise AttributeError('Makes sense only for scheduled orders')
@ -496,13 +496,13 @@ class Order(meta.Base): # noqa:WPS214
@property
def delivery_late(self) -> datetime.timedelta:
"""Time by which a scheduled order was late.
"""Time by which a `.scheduled` order was late.
Measured relative to Order.scheduled_delivery_at.
Measured relative to `.scheduled_delivery_at`.
0 if the delivery is on time or early.
`datetime.timedelta(seconds=0)` if the delivery is on time or early.
Goes together with Order.delivery_early.
Goes together with `.delivery_early`.
"""
if not self.scheduled:
raise AttributeError('Makes sense only for scheduled orders')
@ -510,7 +510,7 @@ class Order(meta.Base): # noqa:WPS214
@property
def total_time(self) -> datetime.timedelta:
"""Time from order placement to delivery for an ad-hoc order."""
"""Time from order placement to delivery for an `.ad_hoc` order."""
if self.scheduled:
raise AttributeError('Scheduled orders have no total_time')
if self.cancelled:

View file

@ -1,4 +1,4 @@
"""Provide the ORM's Restaurant model."""
"""Provide the ORM's `Restaurant` model."""
import sqlalchemy as sa
from sqlalchemy import orm
@ -7,7 +7,12 @@ from urban_meal_delivery.db import meta
class Restaurant(meta.Base):
"""A Restaurant selling meals on the UDP."""
"""A restaurant selling meals on the UDP.
In the historic dataset, a `Restaurant` may have changed its `Address`
throughout its life time. The ORM model only stores the current one,
which in most cases is also the only one.
"""
__tablename__ = 'restaurants'

View file

@ -1,23 +1,23 @@
"""Utils for testing the ORM layer."""
import datetime
import pytest
from alembic import command as migrations_cmd
from alembic import config as migrations_config
from sqlalchemy import orm
from tests.db import fake_data
from urban_meal_delivery import config
from urban_meal_delivery import db
@pytest.fixture(scope='session', params=['all_at_once', 'sequentially'])
def db_engine(request):
def db_connection(request):
"""Create all tables given the ORM models.
The tables are put into a distinct PostgreSQL schema
that is removed after all tests are over.
The engine used to do that is yielded.
The database connection used to do that is yielded.
There are two modes for this fixture:
@ -27,38 +27,40 @@ def db_engine(request):
This ensures that Alembic's migration files are consistent.
"""
engine = db.make_engine()
connection = engine.connect()
if request.param == 'all_at_once':
engine.execute(f'CREATE SCHEMA {config.CLEAN_SCHEMA};')
db.Base.metadata.create_all(engine)
db.Base.metadata.create_all(connection)
else:
cfg = migrations_config.Config('alembic.ini')
migrations_cmd.upgrade(cfg, 'head')
try:
yield engine
yield connection
finally:
engine.execute(f'DROP SCHEMA {config.CLEAN_SCHEMA} CASCADE;')
connection.execute(f'DROP SCHEMA {config.CLEAN_SCHEMA} CASCADE;')
if request.param == 'sequentially':
tmp_alembic_version = f'{config.ALEMBIC_TABLE}_{config.CLEAN_SCHEMA}'
engine.execute(
connection.execute(
f'DROP TABLE {config.ALEMBIC_TABLE_SCHEMA}.{tmp_alembic_version};',
)
connection.close()
@pytest.fixture
def db_session(db_engine):
def db_session(db_connection):
"""A SQLAlchemy session that rolls back everything after a test case."""
connection = db_engine.connect()
# Begin the outer most transaction
# that is rolled back at the end of the test.
transaction = connection.begin()
transaction = db_connection.begin()
# Create a session bound on the same connection as the transaction.
# Using any other session would not work.
Session = db.make_session_factory() # noqa:N806
session = Session(bind=connection)
session_factory = orm.sessionmaker()
session = session_factory(bind=db_connection)
try:
yield session
@ -66,198 +68,20 @@ def db_session(db_engine):
finally:
session.close()
transaction.rollback()
connection.close()
@pytest.fixture
def address_data():
"""The data for an Address object in Paris."""
return {
'id': 1,
'_primary_id': 1, # => "itself"
'created_at': datetime.datetime(2020, 1, 2, 3, 4, 5),
'place_id': 'ChIJxSr71vZt5kcRoFHY4caCCxw',
'latitude': 48.85313,
'longitude': 2.37461,
'_city_id': 1,
'city_name': 'St. German',
'zip_code': '75011',
'street': '42 Rue De Charonne',
'floor': None,
}
# Import the fixtures from the `fake_data` sub-package.
make_address = fake_data.make_address
make_courier = fake_data.make_courier
make_customer = fake_data.make_customer
make_order = fake_data.make_order
make_restaurant = fake_data.make_restaurant
@pytest.fixture
def address(address_data, city):
"""An Address object."""
address = db.Address(**address_data)
address.city = city
return address
@pytest.fixture
def address2_data():
"""The data for an Address object in Paris."""
return {
'id': 2,
'_primary_id': 2, # => "itself"
'created_at': datetime.datetime(2020, 1, 2, 4, 5, 6),
'place_id': 'ChIJs-9a6QZy5kcRY8Wwk9Ywzl8',
'latitude': 48.852196,
'longitude': 2.373937,
'_city_id': 1,
'city_name': 'Paris',
'zip_code': '75011',
'street': 'Rue De Charonne 3',
'floor': 2,
}
@pytest.fixture
def address2(address2_data, city):
"""An Address object."""
address2 = db.Address(**address2_data)
address2.city = city
return address2
@pytest.fixture
def city_data():
"""The data for the City object modeling Paris."""
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):
"""A City object."""
return db.City(**city_data)
@pytest.fixture
def courier_data():
"""The data for a Courier object."""
return {
'id': 1,
'created_at': datetime.datetime(2020, 1, 2, 3, 4, 5),
'vehicle': 'bicycle',
'historic_speed': 7.89,
'capacity': 100,
'pay_per_hour': 750,
'pay_per_order': 200,
}
@pytest.fixture
def courier(courier_data):
"""A Courier object."""
return db.Courier(**courier_data)
@pytest.fixture
def customer_data():
"""The data for the Customer object."""
return {'id': 1}
@pytest.fixture
def customer(customer_data):
"""A Customer object."""
return db.Customer(**customer_data)
@pytest.fixture
def order_data():
"""The data for an ad-hoc Order object."""
return {
'id': 1,
'_delivery_id': 1,
'_customer_id': 1,
'placed_at': datetime.datetime(2020, 1, 2, 11, 55, 11),
'ad_hoc': True,
'scheduled_delivery_at': None,
'scheduled_delivery_at_corrected': None,
'first_estimated_delivery_at': datetime.datetime(2020, 1, 2, 12, 35, 0),
'cancelled': False,
'cancelled_at': None,
'cancelled_at_corrected': None,
'sub_total': 2000,
'delivery_fee': 250,
'total': 2250,
'_restaurant_id': 1,
'restaurant_notified_at': datetime.datetime(2020, 1, 2, 12, 5, 5),
'restaurant_notified_at_corrected': False,
'restaurant_confirmed_at': datetime.datetime(2020, 1, 2, 12, 5, 25),
'restaurant_confirmed_at_corrected': False,
'estimated_prep_duration': 900,
'estimated_prep_duration_corrected': False,
'estimated_prep_buffer': 480,
'_courier_id': 1,
'dispatch_at': datetime.datetime(2020, 1, 2, 12, 5, 1),
'dispatch_at_corrected': False,
'courier_notified_at': datetime.datetime(2020, 1, 2, 12, 6, 2),
'courier_notified_at_corrected': False,
'courier_accepted_at': datetime.datetime(2020, 1, 2, 12, 6, 17),
'courier_accepted_at_corrected': False,
'utilization': 50,
'_pickup_address_id': 1,
'reached_pickup_at': datetime.datetime(2020, 1, 2, 12, 16, 21),
'pickup_at': datetime.datetime(2020, 1, 2, 12, 18, 1),
'pickup_at_corrected': False,
'pickup_not_confirmed': False,
'left_pickup_at': datetime.datetime(2020, 1, 2, 12, 19, 45),
'left_pickup_at_corrected': False,
'_delivery_address_id': 2,
'reached_delivery_at': datetime.datetime(2020, 1, 2, 12, 27, 33),
'delivery_at': datetime.datetime(2020, 1, 2, 12, 29, 55),
'delivery_at_corrected': False,
'delivery_not_confirmed': False,
'_courier_waited_at_delivery': False,
'logged_delivery_distance': 500,
'logged_avg_speed': 7.89,
'logged_avg_speed_distance': 490,
}
@pytest.fixture
def order( # noqa:WPS211 pylint:disable=too-many-arguments
order_data, customer, restaurant, courier, address, address2,
):
"""An Order object."""
order = db.Order(**order_data)
order.customer = customer
order.restaurant = restaurant
order.courier = courier
order.pickup_address = address
order.delivery_address = address2
return order
@pytest.fixture
def restaurant_data():
"""The data for the Restaurant object."""
return {
'id': 1,
'created_at': datetime.datetime(2020, 1, 2, 3, 4, 5),
'name': 'Vevay',
'_address_id': 1,
'estimated_prep_duration': 1000,
}
@pytest.fixture
def restaurant(restaurant_data, address):
"""A Restaurant object."""
restaurant = db.Restaurant(**restaurant_data)
restaurant.address = address
return restaurant
address = fake_data.address
city = fake_data.city
city_data = fake_data.city_data
courier = fake_data.courier
customer = fake_data.customer
order = fake_data.order
restaurant = fake_data.restaurant

View 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

View 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),
)

View 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

View 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)

View file

@ -1,140 +1,123 @@
"""Test the ORM's Address model."""
"""Test the ORM's `Address` model."""
# pylint:disable=no-self-use,protected-access
import pytest
import sqlalchemy as sqla
from sqlalchemy import exc as sa_exc
from sqlalchemy.orm import exc as orm_exc
from urban_meal_delivery import db
class TestSpecialMethods:
"""Test special methods in Address."""
"""Test special methods in `Address`."""
# pylint:disable=no-self-use
def test_create_address(self, address_data):
"""Test instantiation of a new Address object."""
result = db.Address(**address_data)
assert result is not None
def test_text_representation(self, address_data):
"""Address has a non-literal text representation."""
address = db.Address(**address_data)
street = address_data['street']
city_name = address_data['city_name']
def test_create_address(self, address):
"""Test instantiation of a new `Address` object."""
assert address is not None
def test_text_representation(self, address):
"""`Address` has a non-literal text representation."""
result = repr(address)
assert result == f'<Address({street} in {city_name})>'
assert result == f'<Address({address.street} in {address.city_name})>'
@pytest.mark.e2e
@pytest.mark.db
@pytest.mark.no_cover
class TestConstraints:
"""Test the database constraints defined in Address."""
"""Test the database constraints defined in `Address`."""
# pylint:disable=no-self-use
def test_insert_into_database(self, db_session, address):
"""Insert an instance into the (empty) database."""
assert db_session.query(db.Address).count() == 0
def test_insert_into_database(self, address, db_session):
"""Insert an instance into the database."""
db_session.add(address)
db_session.commit()
def test_dublicate_primary_key(self, address, address_data, city, db_session):
"""Can only add a record once."""
assert db_session.query(db.Address).count() == 1
def test_delete_a_referenced_address(self, db_session, address, make_address):
"""Remove a record that is referenced with a FK."""
db_session.add(address)
# Fake another_address that has the same `._primary_id` as `address`.
db_session.add(make_address(_primary_id=address.id))
db_session.commit()
another_address = db.Address(**address_data)
another_address.city = city
db_session.add(another_address)
db_session.delete(address)
with pytest.raises(orm_exc.FlushError):
with pytest.raises(
sa_exc.IntegrityError, match='fk_addresses_to_addresses_via_primary_id',
):
db_session.commit()
def test_delete_a_referenced_address(self, address, address_data, db_session):
def test_delete_a_referenced_city(self, db_session, address):
"""Remove a record that is referenced with a FK."""
db_session.add(address)
db_session.commit()
# Fake a second address that belongs to the same primary address.
address_data['id'] += 1
another_address = db.Address(**address_data)
db_session.add(another_address)
db_session.commit()
# Must delete without ORM as otherwise an UPDATE statement is emitted.
stmt = sqla.delete(db.City).where(db.City.id == address.city.id)
with pytest.raises(sa_exc.IntegrityError):
db_session.execute(
db.Address.__table__.delete().where( # noqa:WPS609
db.Address.id == address.id,
),
)
def test_delete_a_referenced_city(self, address, city, db_session):
"""Remove a record that is referenced with a FK."""
db_session.add(address)
db_session.commit()
with pytest.raises(sa_exc.IntegrityError):
db_session.execute(
db.City.__table__.delete().where(db.City.id == city.id), # noqa:WPS609
)
with pytest.raises(
sa_exc.IntegrityError, match='fk_addresses_to_cities_via_city_id',
):
db_session.execute(stmt)
@pytest.mark.parametrize('latitude', [-91, 91])
def test_invalid_latitude(self, address, db_session, latitude):
def test_invalid_latitude(self, db_session, address, latitude):
"""Insert an instance with invalid data."""
address.latitude = latitude
db_session.add(address)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(
sa_exc.IntegrityError, match='latitude_between_90_degrees',
):
db_session.commit()
@pytest.mark.parametrize('longitude', [-181, 181])
def test_invalid_longitude(self, address, db_session, longitude):
def test_invalid_longitude(self, db_session, address, longitude):
"""Insert an instance with invalid data."""
address.longitude = longitude
db_session.add(address)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(
sa_exc.IntegrityError, match='longitude_between_180_degrees',
):
db_session.commit()
@pytest.mark.parametrize('zip_code', [-1, 0, 9999, 100000])
def test_invalid_zip_code(self, address, db_session, zip_code):
def test_invalid_zip_code(self, db_session, address, zip_code):
"""Insert an instance with invalid data."""
address.zip_code = zip_code
db_session.add(address)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(sa_exc.IntegrityError, match='valid_zip_code'):
db_session.commit()
@pytest.mark.parametrize('floor', [-1, 41])
def test_invalid_floor(self, address, db_session, floor):
def test_invalid_floor(self, db_session, address, floor):
"""Insert an instance with invalid data."""
address.floor = floor
db_session.add(address)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(sa_exc.IntegrityError, match='realistic_floor'):
db_session.commit()
class TestProperties:
"""Test properties in Address."""
"""Test properties in `Address`."""
# pylint:disable=no-self-use
def test_is_primary(self, address_data):
"""Test Address.is_primary property."""
address = db.Address(**address_data)
def test_is_primary(self, address):
"""Test `Address.is_primary` property."""
assert address.id == address._primary_id # noqa:WPS437
result = address.is_primary
assert result is True
def test_is_not_primary(self, address_data):
"""Test Address.is_primary property."""
address_data['_primary_id'] = 999
address = db.Address(**address_data)
def test_is_not_primary(self, address):
"""Test `Address.is_primary` property."""
address._primary_id = 999 # noqa:WPS437
result = address.is_primary

View file

@ -1,65 +1,45 @@
"""Test the ORM's City model."""
"""Test the ORM's `City` model."""
# pylint:disable=no-self-use
import pytest
from sqlalchemy.orm import exc as orm_exc
from urban_meal_delivery import db
class TestSpecialMethods:
"""Test special methods in City."""
"""Test special methods in `City`."""
# pylint:disable=no-self-use
def test_create_city(self, city_data):
"""Test instantiation of a new City object."""
result = db.City(**city_data)
assert result is not None
def test_text_representation(self, city_data):
"""City has a non-literal text representation."""
city = db.City(**city_data)
name = city_data['name']
def test_create_city(self, city):
"""Test instantiation of a new `City` object."""
assert city is not None
def test_text_representation(self, city):
"""`City` has a non-literal text representation."""
result = repr(city)
assert result == f'<City({name})>'
assert result == f'<City({city.name})>'
@pytest.mark.e2e
@pytest.mark.db
@pytest.mark.no_cover
class TestConstraints:
"""Test the database constraints defined in City."""
"""Test the database constraints defined in `City`."""
# pylint:disable=no-self-use
def test_insert_into_database(self, db_session, city):
"""Insert an instance into the (empty) database."""
assert db_session.query(db.City).count() == 0
def test_insert_into_database(self, city, db_session):
"""Insert an instance into the database."""
db_session.add(city)
db_session.commit()
def test_dublicate_primary_key(self, city, city_data, db_session):
"""Can only add a record once."""
db_session.add(city)
db_session.commit()
another_city = db.City(**city_data)
db_session.add(another_city)
with pytest.raises(orm_exc.FlushError):
db_session.commit()
assert db_session.query(db.City).count() == 1
class TestProperties:
"""Test properties in City."""
# pylint:disable=no-self-use
def test_location_data(self, city_data):
"""Test City.location property."""
city = db.City(**city_data)
"""Test properties in `City`."""
def test_location_data(self, city, city_data):
"""Test `City.location` property."""
result = city.location
assert isinstance(result, dict)
@ -67,33 +47,19 @@ class TestProperties:
assert result['latitude'] == pytest.approx(city_data['_center_latitude'])
assert result['longitude'] == pytest.approx(city_data['_center_longitude'])
def test_viewport_data_overall(self, city_data):
"""Test City.viewport property."""
city = db.City(**city_data)
def test_viewport_data_overall(self, city):
"""Test `City.viewport` property."""
result = city.viewport
assert isinstance(result, dict)
assert len(result) == 2
def test_viewport_data_northeast(self, city_data):
"""Test City.viewport property."""
city = db.City(**city_data)
result = city.viewport['northeast']
@pytest.mark.parametrize('corner', ['northeast', 'southwest'])
def test_viewport_data_corners(self, city, city_data, corner):
"""Test `City.viewport` property."""
result = city.viewport[corner]
assert isinstance(result, dict)
assert len(result) == 2
assert result['latitude'] == pytest.approx(city_data['_northeast_latitude'])
assert result['longitude'] == pytest.approx(city_data['_northeast_longitude'])
def test_viewport_data_southwest(self, city_data):
"""Test City.viewport property."""
city = db.City(**city_data)
result = city.viewport['southwest']
assert isinstance(result, dict)
assert len(result) == 2
assert result['latitude'] == pytest.approx(city_data['_southwest_latitude'])
assert result['longitude'] == pytest.approx(city_data['_southwest_longitude'])
assert result['latitude'] == pytest.approx(city_data[f'_{corner}_latitude'])
assert result['longitude'] == pytest.approx(city_data[f'_{corner}_longitude'])

View file

@ -1,125 +1,108 @@
"""Test the ORM's Courier model."""
"""Test the ORM's `Courier` model."""
# pylint:disable=no-self-use
import pytest
from sqlalchemy import exc as sa_exc
from sqlalchemy.orm import exc as orm_exc
from urban_meal_delivery import db
class TestSpecialMethods:
"""Test special methods in Courier."""
"""Test special methods in `Courier`."""
# pylint:disable=no-self-use
def test_create_courier(self, courier_data):
"""Test instantiation of a new Courier object."""
result = db.Courier(**courier_data)
assert result is not None
def test_text_representation(self, courier_data):
"""Courier has a non-literal text representation."""
courier_data['id'] = 1
courier = db.Courier(**courier_data)
id_ = courier_data['id']
def test_create_courier(self, courier):
"""Test instantiation of a new `Courier` object."""
assert courier is not None
def test_text_representation(self, courier):
"""`Courier` has a non-literal text representation."""
result = repr(courier)
assert result == f'<Courier(#{id_})>'
assert result == f'<Courier(#{courier.id})>'
@pytest.mark.e2e
@pytest.mark.db
@pytest.mark.no_cover
class TestConstraints:
"""Test the database constraints defined in Courier."""
"""Test the database constraints defined in `Courier`."""
# pylint:disable=no-self-use
def test_insert_into_database(self, db_session, courier):
"""Insert an instance into the (empty) database."""
assert db_session.query(db.Courier).count() == 0
def test_insert_into_database(self, courier, db_session):
"""Insert an instance into the database."""
db_session.add(courier)
db_session.commit()
def test_dublicate_primary_key(self, courier, courier_data, db_session):
"""Can only add a record once."""
db_session.add(courier)
db_session.commit()
assert db_session.query(db.Courier).count() == 1
another_courier = db.Courier(**courier_data)
db_session.add(another_courier)
with pytest.raises(orm_exc.FlushError):
db_session.commit()
def test_invalid_vehicle(self, courier, db_session):
def test_invalid_vehicle(self, db_session, courier):
"""Insert an instance with invalid data."""
courier.vehicle = 'invalid'
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(sa_exc.IntegrityError, match='available_vehicle_types'):
db_session.commit()
def test_negative_speed(self, courier, db_session):
def test_negative_speed(self, db_session, courier):
"""Insert an instance with invalid data."""
courier.historic_speed = -1
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(sa_exc.IntegrityError, match='realistic_speed'):
db_session.commit()
def test_unrealistic_speed(self, courier, db_session):
def test_unrealistic_speed(self, db_session, courier):
"""Insert an instance with invalid data."""
courier.historic_speed = 999
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(sa_exc.IntegrityError, match='realistic_speed'):
db_session.commit()
def test_negative_capacity(self, courier, db_session):
def test_negative_capacity(self, db_session, courier):
"""Insert an instance with invalid data."""
courier.capacity = -1
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(sa_exc.IntegrityError, match='capacity_under_200_liters'):
db_session.commit()
def test_too_much_capacity(self, courier, db_session):
def test_too_much_capacity(self, db_session, courier):
"""Insert an instance with invalid data."""
courier.capacity = 999
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(sa_exc.IntegrityError, match='capacity_under_200_liters'):
db_session.commit()
def test_negative_pay_per_hour(self, courier, db_session):
def test_negative_pay_per_hour(self, db_session, courier):
"""Insert an instance with invalid data."""
courier.pay_per_hour = -1
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(sa_exc.IntegrityError, match='realistic_pay_per_hour'):
db_session.commit()
def test_too_much_pay_per_hour(self, courier, db_session):
def test_too_much_pay_per_hour(self, db_session, courier):
"""Insert an instance with invalid data."""
courier.pay_per_hour = 9999
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(sa_exc.IntegrityError, match='realistic_pay_per_hour'):
db_session.commit()
def test_negative_pay_per_order(self, courier, db_session):
def test_negative_pay_per_order(self, db_session, courier):
"""Insert an instance with invalid data."""
courier.pay_per_order = -1
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(sa_exc.IntegrityError, match='realistic_pay_per_order'):
db_session.commit()
def test_too_much_pay_per_order(self, courier, db_session):
def test_too_much_pay_per_order(self, db_session, courier):
"""Insert an instance with invalid data."""
courier.pay_per_order = 999
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(sa_exc.IntegrityError, match='realistic_pay_per_order'):
db_session.commit()

View file

@ -1,51 +1,35 @@
"""Test the ORM's Customer model."""
"""Test the ORM's `Customer` model."""
# pylint:disable=no-self-use
import pytest
from sqlalchemy.orm import exc as orm_exc
from urban_meal_delivery import db
class TestSpecialMethods:
"""Test special methods in Customer."""
"""Test special methods in `Customer`."""
# pylint:disable=no-self-use
def test_create_customer(self, customer_data):
"""Test instantiation of a new Customer object."""
result = db.Customer(**customer_data)
assert result is not None
def test_text_representation(self, customer_data):
"""Customer has a non-literal text representation."""
customer = db.Customer(**customer_data)
id_ = customer_data['id']
def test_create_customer(self, customer):
"""Test instantiation of a new `Customer` object."""
assert customer is not None
def test_text_representation(self, customer):
"""`Customer` has a non-literal text representation."""
result = repr(customer)
assert result == f'<Customer(#{id_})>'
assert result == f'<Customer(#{customer.id})>'
@pytest.mark.e2e
@pytest.mark.db
@pytest.mark.no_cover
class TestConstraints:
"""Test the database constraints defined in Customer."""
"""Test the database constraints defined in `Customer`."""
# pylint:disable=no-self-use
def test_insert_into_database(self, db_session, customer):
"""Insert an instance into the (empty) database."""
assert db_session.query(db.Customer).count() == 0
def test_insert_into_database(self, customer, db_session):
"""Insert an instance into the database."""
db_session.add(customer)
db_session.commit()
def test_dublicate_primary_key(self, customer, customer_data, db_session):
"""Can only add a record once."""
db_session.add(customer)
db_session.commit()
another_customer = db.Customer(**customer_data)
db_session.add(another_customer)
with pytest.raises(orm_exc.FlushError):
db_session.commit()
assert db_session.query(db.Customer).count() == 1

View file

@ -1,57 +1,41 @@
"""Test the ORM's Order model."""
"""Test the ORM's `Order` model."""
# pylint:disable=no-self-use,protected-access
import datetime
import random
import pytest
from sqlalchemy.orm import exc as orm_exc
from urban_meal_delivery import db
class TestSpecialMethods:
"""Test special methods in Order."""
"""Test special methods in `Order`."""
# pylint:disable=no-self-use
def test_create_order(self, order_data):
"""Test instantiation of a new Order object."""
result = db.Order(**order_data)
assert result is not None
def test_text_representation(self, order_data):
"""Order has a non-literal text representation."""
order = db.Order(**order_data)
id_ = order_data['id']
def test_create_order(self, order):
"""Test instantiation of a new `Order` object."""
assert order is not None
def test_text_representation(self, order):
"""`Order` has a non-literal text representation."""
result = repr(order)
assert result == f'<Order(#{id_})>'
assert result == f'<Order(#{order.id})>'
@pytest.mark.e2e
@pytest.mark.db
@pytest.mark.no_cover
class TestConstraints:
"""Test the database constraints defined in Order."""
"""Test the database constraints defined in `Order`."""
# pylint:disable=no-self-use
def test_insert_into_database(self, db_session, order):
"""Insert an instance into the (empty) database."""
assert db_session.query(db.Order).count() == 0
def test_insert_into_database(self, order, db_session):
"""Insert an instance into the database."""
db_session.add(order)
db_session.commit()
def test_dublicate_primary_key(self, order, order_data, city, db_session):
"""Can only add a record once."""
db_session.add(order)
db_session.commit()
another_order = db.Order(**order_data)
another_order.city = city
db_session.add(another_order)
with pytest.raises(orm_exc.FlushError):
db_session.commit()
assert db_session.query(db.Order).count() == 1
# TODO (order-constraints): the various Foreign Key and Check Constraints
# should be tested eventually. This is not of highest importance as
@ -59,339 +43,431 @@ class TestConstraints:
class TestProperties:
"""Test properties in Order."""
"""Test properties in `Order`.
The `order` fixture uses the defaults specified in `factories.OrderFactory`
and provided by the `make_order` fixture.
"""
# pylint:disable=no-self-use,too-many-public-methods
def test_is_not_scheduled(self, order_data):
"""Test Order.scheduled property."""
order = db.Order(**order_data)
def test_is_ad_hoc(self, order):
"""Test `Order.scheduled` property."""
assert order.ad_hoc is True
result = order.scheduled
assert result is False
def test_is_scheduled(self, order_data):
"""Test Order.scheduled property."""
order_data['ad_hoc'] = False
order_data['scheduled_delivery_at'] = datetime.datetime(2020, 1, 2, 12, 30, 0)
order_data['scheduled_delivery_at_corrected'] = False
order = db.Order(**order_data)
def test_is_scheduled(self, make_order):
"""Test `Order.scheduled` property."""
order = make_order(scheduled=True)
assert order.ad_hoc is False
result = order.scheduled
assert result is True
def test_is_completed(self, order_data):
"""Test Order.completed property."""
order = db.Order(**order_data)
def test_is_completed(self, order):
"""Test `Order.completed` property."""
result = order.completed
assert result is True
def test_is_not_completed(self, order_data):
"""Test Order.completed property."""
order_data['cancelled'] = True
order_data['cancelled_at'] = datetime.datetime(2020, 1, 2, 12, 15, 0)
order_data['cancelled_at_corrected'] = False
order = db.Order(**order_data)
def test_is_not_completed1(self, make_order):
"""Test `Order.completed` property."""
order = make_order(cancel_before_pickup=True)
assert order.cancelled is True
result = order.completed
assert result is False
def test_is_corrected(self, order_data):
"""Test Order.corrected property."""
order_data['dispatch_at_corrected'] = True
order = db.Order(**order_data)
def test_is_not_completed2(self, make_order):
"""Test `Order.completed` property."""
order = make_order(cancel_after_pickup=True)
assert order.cancelled is True
result = order.completed
assert result is False
def test_is_not_corrected(self, order):
"""Test `Order.corrected` property."""
# By default, the `OrderFactory` sets all `.*_corrected` attributes to `False`.
result = order.corrected
assert result is False
@pytest.mark.parametrize(
'column',
[
'scheduled_delivery_at',
'cancelled_at',
'restaurant_notified_at',
'restaurant_confirmed_at',
'dispatch_at',
'courier_notified_at',
'courier_accepted_at',
'pickup_at',
'left_pickup_at',
'delivery_at',
],
)
def test_is_corrected(self, order, column):
"""Test `Order.corrected` property."""
setattr(order, f'{column}_corrected', True)
result = order.corrected
assert result is True
def test_time_to_accept_no_dispatch_at(self, order_data):
"""Test Order.time_to_accept property."""
order_data['dispatch_at'] = None
order = db.Order(**order_data)
def test_time_to_accept_no_dispatch_at(self, order):
"""Test `Order.time_to_accept` property."""
order.dispatch_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.time_to_accept)
def test_time_to_accept_no_courier_accepted(self, order_data):
"""Test Order.time_to_accept property."""
order_data['courier_accepted_at'] = None
order = db.Order(**order_data)
def test_time_to_accept_no_courier_accepted(self, order):
"""Test `Order.time_to_accept` property."""
order.courier_accepted_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.time_to_accept)
def test_time_to_accept_success(self, order_data):
"""Test Order.time_to_accept property."""
order = db.Order(**order_data)
def test_time_to_accept_success(self, order):
"""Test `Order.time_to_accept` property."""
result = order.time_to_accept
assert isinstance(result, datetime.timedelta)
assert result > datetime.timedelta(0)
def test_time_to_react_no_courier_notified(self, order_data):
"""Test Order.time_to_react property."""
order_data['courier_notified_at'] = None
order = db.Order(**order_data)
def test_time_to_react_no_courier_notified(self, order):
"""Test `Order.time_to_react` property."""
order.courier_notified_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.time_to_react)
def test_time_to_react_no_courier_accepted(self, order_data):
"""Test Order.time_to_react property."""
order_data['courier_accepted_at'] = None
order = db.Order(**order_data)
def test_time_to_react_no_courier_accepted(self, order):
"""Test `Order.time_to_react` property."""
order.courier_accepted_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.time_to_react)
def test_time_to_react_success(self, order_data):
"""Test Order.time_to_react property."""
order = db.Order(**order_data)
def test_time_to_react_success(self, order):
"""Test `Order.time_to_react` property."""
result = order.time_to_react
assert isinstance(result, datetime.timedelta)
assert result > datetime.timedelta(0)
def test_time_to_pickup_no_reached_pickup_at(self, order_data):
"""Test Order.time_to_pickup property."""
order_data['reached_pickup_at'] = None
order = db.Order(**order_data)
def test_time_to_pickup_no_reached_pickup_at(self, order):
"""Test `Order.time_to_pickup` property."""
order.reached_pickup_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.time_to_pickup)
def test_time_to_pickup_no_courier_accepted(self, order_data):
"""Test Order.time_to_pickup property."""
order_data['courier_accepted_at'] = None
order = db.Order(**order_data)
def test_time_to_pickup_no_courier_accepted(self, order):
"""Test `Order.time_to_pickup` property."""
order.courier_accepted_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.time_to_pickup)
def test_time_to_pickup_success(self, order_data):
"""Test Order.time_to_pickup property."""
order = db.Order(**order_data)
def test_time_to_pickup_success(self, order):
"""Test `Order.time_to_pickup` property."""
result = order.time_to_pickup
assert isinstance(result, datetime.timedelta)
assert result > datetime.timedelta(0)
def test_time_at_pickup_no_reached_pickup_at(self, order_data):
"""Test Order.time_at_pickup property."""
order_data['reached_pickup_at'] = None
order = db.Order(**order_data)
def test_time_at_pickup_no_reached_pickup_at(self, order):
"""Test `Order.time_at_pickup` property."""
order.reached_pickup_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.time_at_pickup)
def test_time_at_pickup_no_pickup_at(self, order_data):
"""Test Order.time_at_pickup property."""
order_data['pickup_at'] = None
order = db.Order(**order_data)
def test_time_at_pickup_no_pickup_at(self, order):
"""Test `Order.time_at_pickup` property."""
order.pickup_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.time_at_pickup)
def test_time_at_pickup_success(self, order_data):
"""Test Order.time_at_pickup property."""
order = db.Order(**order_data)
def test_time_at_pickup_success(self, order):
"""Test `Order.time_at_pickup` property."""
result = order.time_at_pickup
assert isinstance(result, datetime.timedelta)
assert result > datetime.timedelta(0)
def test_scheduled_pickup_at_no_restaurant_notified( # noqa:WPS118
self, order_data,
):
"""Test Order.scheduled_pickup_at property."""
order_data['restaurant_notified_at'] = None
order = db.Order(**order_data)
def test_scheduled_pickup_at_no_restaurant_notified(self, order): # noqa:WPS118
"""Test `Order.scheduled_pickup_at` property."""
order.restaurant_notified_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.scheduled_pickup_at)
def test_scheduled_pickup_at_no_est_prep_duration(self, order_data): # noqa:WPS118
"""Test Order.scheduled_pickup_at property."""
order_data['estimated_prep_duration'] = None
order = db.Order(**order_data)
def test_scheduled_pickup_at_no_est_prep_duration(self, order): # noqa:WPS118
"""Test `Order.scheduled_pickup_at` property."""
order.estimated_prep_duration = None
with pytest.raises(RuntimeError, match='not set'):
int(order.scheduled_pickup_at)
def test_scheduled_pickup_at_success(self, order_data):
"""Test Order.scheduled_pickup_at property."""
order = db.Order(**order_data)
def test_scheduled_pickup_at_success(self, order):
"""Test `Order.scheduled_pickup_at` property."""
result = order.scheduled_pickup_at
assert isinstance(result, datetime.datetime)
assert order.placed_at < result < order.delivery_at
def test_if_courier_early_at_pickup(self, order_data):
"""Test Order.courier_early property."""
order = db.Order(**order_data)
def test_courier_is_early_at_pickup(self, order):
"""Test `Order.courier_early` property."""
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
order.estimated_prep_duration = 999_999
result = order.courier_early
assert bool(result) is True
def test_if_courier_late_at_pickup(self, order_data):
"""Test Order.courier_late property."""
# Opposite of test case before.
order = db.Order(**order_data)
def test_courier_is_not_early_at_pickup(self, order):
"""Test `Order.courier_early` property."""
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
order.estimated_prep_duration = 1
result = order.courier_early
assert bool(result) is False
def test_courier_is_late_at_pickup(self, order):
"""Test `Order.courier_late` property."""
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
order.estimated_prep_duration = 1
result = order.courier_late
assert bool(result) is True
def test_courier_is_not_late_at_pickup(self, order):
"""Test `Order.courier_late` property."""
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
order.estimated_prep_duration = 999_999
result = order.courier_late
assert bool(result) is False
def test_if_restaurant_early_at_pickup(self, order_data):
"""Test Order.restaurant_early property."""
order = db.Order(**order_data)
def test_restaurant_early_at_pickup(self, order):
"""Test `Order.restaurant_early` property."""
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
order.estimated_prep_duration = 999_999
result = order.restaurant_early
assert bool(result) is True
def test_if_restaurant_late_at_pickup(self, order_data):
"""Test Order.restaurant_late property."""
# Opposite of test case before.
order = db.Order(**order_data)
def test_restaurant_is_not_early_at_pickup(self, order):
"""Test `Order.restaurant_early` property."""
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
order.estimated_prep_duration = 1
result = order.restaurant_early
assert bool(result) is False
def test_restaurant_is_late_at_pickup(self, order):
"""Test `Order.restaurant_late` property."""
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
order.estimated_prep_duration = 1
result = order.restaurant_late
assert bool(result) is True
def test_restaurant_is_not_late_at_pickup(self, order):
"""Test `Order.restaurant_late` property."""
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
order.estimated_prep_duration = 999_999
result = order.restaurant_late
assert bool(result) is False
def test_time_to_delivery_no_reached_delivery_at(self, order_data): # noqa:WPS118
"""Test Order.time_to_delivery property."""
order_data['reached_delivery_at'] = None
order = db.Order(**order_data)
def test_time_to_delivery_no_reached_delivery_at(self, order): # noqa:WPS118
"""Test `Order.time_to_delivery` property."""
order.reached_delivery_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.time_to_delivery)
def test_time_to_delivery_no_pickup_at(self, order_data):
"""Test Order.time_to_delivery property."""
order_data['pickup_at'] = None
order = db.Order(**order_data)
def test_time_to_delivery_no_pickup_at(self, order):
"""Test `Order.time_to_delivery` property."""
order.pickup_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.time_to_delivery)
def test_time_to_delivery_success(self, order_data):
"""Test Order.time_to_delivery property."""
order = db.Order(**order_data)
def test_time_to_delivery_success(self, order):
"""Test `Order.time_to_delivery` property."""
result = order.time_to_delivery
assert isinstance(result, datetime.timedelta)
assert result > datetime.timedelta(0)
def test_time_at_delivery_no_reached_delivery_at(self, order_data): # noqa:WPS118
"""Test Order.time_at_delivery property."""
order_data['reached_delivery_at'] = None
order = db.Order(**order_data)
def test_time_at_delivery_no_reached_delivery_at(self, order): # noqa:WPS118
"""Test `Order.time_at_delivery` property."""
order.reached_delivery_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.time_at_delivery)
def test_time_at_delivery_no_delivery_at(self, order_data):
"""Test Order.time_at_delivery property."""
order_data['delivery_at'] = None
order = db.Order(**order_data)
def test_time_at_delivery_no_delivery_at(self, order):
"""Test `Order.time_at_delivery` property."""
order.delivery_at = None
with pytest.raises(RuntimeError, match='not set'):
int(order.time_at_delivery)
def test_time_at_delivery_success(self, order_data):
"""Test Order.time_at_delivery property."""
order = db.Order(**order_data)
def test_time_at_delivery_success(self, order):
"""Test `Order.time_at_delivery` property."""
result = order.time_at_delivery
assert isinstance(result, datetime.timedelta)
assert result > datetime.timedelta(0)
def test_courier_waited_at_delviery(self, order_data):
"""Test Order.courier_waited_at_delivery property."""
order_data['_courier_waited_at_delivery'] = True
order = db.Order(**order_data)
def test_courier_waited_at_delviery(self, order):
"""Test `Order.courier_waited_at_delivery` property."""
order._courier_waited_at_delivery = True # noqa:WPS437
result = int(order.courier_waited_at_delivery.total_seconds())
result = order.courier_waited_at_delivery.total_seconds()
assert result > 0
def test_courier_did_not_wait_at_delivery(self, order_data):
"""Test Order.courier_waited_at_delivery property."""
order_data['_courier_waited_at_delivery'] = False
order = db.Order(**order_data)
def test_courier_did_not_wait_at_delivery(self, order):
"""Test `Order.courier_waited_at_delivery` property."""
order._courier_waited_at_delivery = False # noqa:WPS437
result = int(order.courier_waited_at_delivery.total_seconds())
result = order.courier_waited_at_delivery.total_seconds()
assert result == 0
def test_if_delivery_early_success(self, order_data):
"""Test Order.delivery_early property."""
order_data['ad_hoc'] = False
order_data['scheduled_delivery_at'] = datetime.datetime(2020, 1, 2, 12, 30, 0)
order_data['scheduled_delivery_at_corrected'] = False
order = db.Order(**order_data)
def test_ad_hoc_order_cannot_be_early(self, order):
"""Test `Order.delivery_early` property."""
# By default, the `OrderFactory` creates ad-hoc orders.
with pytest.raises(AttributeError, match='scheduled'):
int(order.delivery_early)
def test_scheduled_order_delivered_early(self, make_order):
"""Test `Order.delivery_early` property."""
order = make_order(scheduled=True)
# Schedule the order to a lot later.
order.scheduled_delivery_at += datetime.timedelta(hours=2)
result = order.delivery_early
assert bool(result) is True
def test_if_delivery_early_failure(self, order_data):
"""Test Order.delivery_early property."""
order = db.Order(**order_data)
def test_scheduled_order_not_delivered_early(self, make_order):
"""Test `Order.delivery_early` property."""
order = make_order(scheduled=True)
# Schedule the order to a lot earlier.
order.scheduled_delivery_at -= datetime.timedelta(hours=2)
with pytest.raises(AttributeError, match='scheduled'):
int(order.delivery_early)
result = order.delivery_early
def test_if_delivery_late_success(self, order_data):
assert bool(result) is False
def test_ad_hoc_order_cannot_be_late(self, order):
"""Test Order.delivery_late property."""
order_data['ad_hoc'] = False
order_data['scheduled_delivery_at'] = datetime.datetime(2020, 1, 2, 12, 30, 0)
order_data['scheduled_delivery_at_corrected'] = False
order = db.Order(**order_data)
# By default, the `OrderFactory` creates ad-hoc orders.
with pytest.raises(AttributeError, match='scheduled'):
int(order.delivery_late)
def test_scheduled_order_delivered_late(self, make_order):
"""Test `Order.delivery_early` property."""
order = make_order(scheduled=True)
# Schedule the order to a lot earlier.
order.scheduled_delivery_at -= datetime.timedelta(hours=2)
result = order.delivery_late
assert bool(result) is True
def test_scheduled_order_not_delivered_late(self, make_order):
"""Test `Order.delivery_early` property."""
order = make_order(scheduled=True)
# Schedule the order to a lot later.
order.scheduled_delivery_at += datetime.timedelta(hours=2)
result = order.delivery_late
assert bool(result) is False
def test_if_delivery_late_failure(self, order_data):
"""Test Order.delivery_late property."""
order = db.Order(**order_data)
with pytest.raises(AttributeError, match='scheduled'):
int(order.delivery_late)
def test_no_total_time_for_pre_order(self, order_data):
"""Test Order.total_time property."""
order_data['ad_hoc'] = False
order_data['scheduled_delivery_at'] = datetime.datetime(2020, 1, 2, 12, 30, 0)
order_data['scheduled_delivery_at_corrected'] = False
order = db.Order(**order_data)
def test_no_total_time_for_scheduled_order(self, make_order):
"""Test `Order.total_time` property."""
order = make_order(scheduled=True)
with pytest.raises(AttributeError, match='Scheduled'):
int(order.total_time)
def test_no_total_time_for_cancelled_order(self, order_data):
"""Test Order.total_time property."""
order_data['cancelled'] = True
order_data['cancelled_at'] = datetime.datetime(2020, 1, 2, 12, 15, 0)
order_data['cancelled_at_corrected'] = False
order = db.Order(**order_data)
def test_no_total_time_for_cancelled_order(self, make_order):
"""Test `Order.total_time` property."""
order = make_order(cancel_before_pickup=True)
with pytest.raises(RuntimeError, match='Cancelled'):
int(order.total_time)
def test_total_time_success(self, order_data):
"""Test Order.total_time property."""
order = db.Order(**order_data)
def test_total_time_success(self, order):
"""Test `Order.total_time` property."""
result = order.total_time
assert isinstance(result, datetime.timedelta)
assert result > datetime.timedelta(0)
@pytest.mark.db
@pytest.mark.no_cover
def test_make_random_orders( # noqa:C901,WPS211,WPS210,WPS213,WPS231
db_session, make_address, make_courier, make_restaurant, make_order,
):
"""Sanity check the all the `make_*` fixtures.
Ensure that all generated `Address`, `Courier`, `Customer`, `Restauarant`,
and `Order` objects adhere to the database constraints.
""" # noqa:D202
# Generate a large number of `Order`s to obtain a large variance of data.
for _ in range(1_000): # noqa:WPS122
# Ad-hoc `Order`s are far more common than pre-orders.
scheduled = random.choice([True, False, False, False, False])
# Randomly pass a `address` argument to `make_restaurant()` and
# a `restaurant` argument to `make_order()`.
if random.random() < 0.5:
address = random.choice([None, make_address()])
restaurant = make_restaurant(address=address)
else:
restaurant = None
# Randomly pass a `courier` argument to `make_order()`.
courier = random.choice([None, make_courier()])
# A tiny fraction of `Order`s get cancelled.
if random.random() < 0.05:
if random.random() < 0.5:
cancel_before_pickup, cancel_after_pickup = True, False
else:
cancel_before_pickup, cancel_after_pickup = False, True
else:
cancel_before_pickup, cancel_after_pickup = False, False
# Write all the generated objects to the database.
# This should already trigger an `IntegrityError` if the data are flawed.
order = make_order(
scheduled=scheduled,
restaurant=restaurant,
courier=courier,
cancel_before_pickup=cancel_before_pickup,
cancel_after_pickup=cancel_after_pickup,
)
db_session.add(order)
db_session.commit()

View file

@ -1,80 +1,70 @@
"""Test the ORM's Restaurant model."""
"""Test the ORM's `Restaurant` model."""
# pylint:disable=no-self-use
import pytest
import sqlalchemy as sqla
from sqlalchemy import exc as sa_exc
from sqlalchemy.orm import exc as orm_exc
from urban_meal_delivery import db
class TestSpecialMethods:
"""Test special methods in Restaurant."""
"""Test special methods in `Restaurant`."""
# pylint:disable=no-self-use
def test_create_restaurant(self, restaurant_data):
"""Test instantiation of a new Restaurant object."""
result = db.Restaurant(**restaurant_data)
assert result is not None
def test_text_representation(self, restaurant_data):
"""Restaurant has a non-literal text representation."""
restaurant = db.Restaurant(**restaurant_data)
name = restaurant_data['name']
def test_create_restaurant(self, restaurant):
"""Test instantiation of a new `Restaurant` object."""
assert restaurant is not None
def test_text_representation(self, restaurant):
"""`Restaurant` has a non-literal text representation."""
result = repr(restaurant)
assert result == f'<Restaurant({name})>'
assert result == f'<Restaurant({restaurant.name})>'
@pytest.mark.e2e
@pytest.mark.db
@pytest.mark.no_cover
class TestConstraints:
"""Test the database constraints defined in Restaurant."""
"""Test the database constraints defined in `Restaurant`."""
# pylint:disable=no-self-use
def test_insert_into_database(self, db_session, restaurant):
"""Insert an instance into the (empty) database."""
assert db_session.query(db.Restaurant).count() == 0
def test_insert_into_database(self, restaurant, db_session):
"""Insert an instance into the database."""
db_session.add(restaurant)
db_session.commit()
def test_dublicate_primary_key(self, restaurant, restaurant_data, db_session):
"""Can only add a record once."""
db_session.add(restaurant)
db_session.commit()
assert db_session.query(db.Restaurant).count() == 1
another_restaurant = db.Restaurant(**restaurant_data)
db_session.add(another_restaurant)
with pytest.raises(orm_exc.FlushError):
db_session.commit()
def test_delete_a_referenced_address(self, restaurant, address, db_session):
def test_delete_a_referenced_address(self, db_session, restaurant):
"""Remove a record that is referenced with a FK."""
db_session.add(restaurant)
db_session.commit()
with pytest.raises(sa_exc.IntegrityError):
db_session.execute(
db.Address.__table__.delete().where( # noqa:WPS609
db.Address.id == address.id,
),
)
# Must delete without ORM as otherwise an UPDATE statement is emitted.
stmt = sqla.delete(db.Address).where(db.Address.id == restaurant.address.id)
def test_negative_prep_duration(self, restaurant, db_session):
with pytest.raises(
sa_exc.IntegrityError, match='fk_restaurants_to_addresses_via_address_id',
):
db_session.execute(stmt)
def test_negative_prep_duration(self, db_session, restaurant):
"""Insert an instance with invalid data."""
restaurant.estimated_prep_duration = -1
db_session.add(restaurant)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(
sa_exc.IntegrityError, match='realistic_estimated_prep_duration',
):
db_session.commit()
def test_too_high_prep_duration(self, restaurant, db_session):
def test_too_high_prep_duration(self, db_session, restaurant):
"""Insert an instance with invalid data."""
restaurant.estimated_prep_duration = 2500
db_session.add(restaurant)
with pytest.raises(sa_exc.IntegrityError):
with pytest.raises(
sa_exc.IntegrityError, match='realistic_estimated_prep_duration',
):
db_session.commit()