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