Add an ORM layer

- use SQLAlchemy (and PostgreSQL) to model the ORM layer
- add the following models:
  + Address => modelling all kinds of addresses
  + City => model the three target cities
  + Courier => model the UDP's couriers
  + Customer => model the UDP's customers
  + Order => model the orders received by the UDP
  + Restaurant => model the restaurants active on the UDP
- so far, the emphasis lies on expression the Foreign Key
  and Check Constraints that are used to validate the assumptions
  inherent to the cleanded data
- provide database-independent unit tests with 100% coverage
- provide additional integration tests ("e2e") that commit data to
  a PostgreSQL instance to validate that the constraints work
- adapt linting rules a bit
This commit is contained in:
Alexander Hess 2020-08-09 03:45:19 +02:00
commit fdcc93a1ea
Signed by: alexander
GPG key ID: 344EA5AB10D868E0
24 changed files with 2119 additions and 4 deletions

1
tests/db/__init__.py Normal file
View file

@ -0,0 +1 @@
"""Test the ORM layer."""

244
tests/db/conftest.py Normal file
View file

@ -0,0 +1,244 @@
"""Utils for testing the ORM layer."""
import datetime
import pytest
from sqlalchemy import schema
from urban_meal_delivery import config
from urban_meal_delivery import db
@pytest.fixture(scope='session')
def db_engine():
"""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.
"""
engine = db.make_engine()
engine.execute(schema.CreateSchema(config.CLEAN_SCHEMA))
db.Base.metadata.create_all(engine)
try:
yield engine
finally:
engine.execute(schema.DropSchema(config.CLEAN_SCHEMA, cascade=True))
@pytest.fixture
def db_session(db_engine):
"""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()
# 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)
try:
yield session
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,
}
@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

141
tests/db/test_addresses.py Normal file
View file

@ -0,0 +1,141 @@
"""Test the ORM's Address model."""
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 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']
result = repr(address)
assert result == f'<Address({street} in {city_name})>'
@pytest.mark.e2e
@pytest.mark.no_cover
class TestConstraints:
"""Test the database constraints defined in Address."""
# pylint:disable=no-self-use
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."""
db_session.add(address)
db_session.commit()
another_address = db.Address(**address_data)
another_address.city = city
db_session.add(another_address)
with pytest.raises(orm_exc.FlushError):
db_session.commit()
def test_delete_a_referenced_address(self, address, address_data, db_session):
"""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()
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
)
@pytest.mark.parametrize('latitude', [-91, 91])
def test_invalid_latitude(self, address, db_session, latitude):
"""Insert an instance with invalid data."""
address.latitude = latitude
db_session.add(address)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
@pytest.mark.parametrize('longitude', [-181, 181])
def test_invalid_longitude(self, address, db_session, longitude):
"""Insert an instance with invalid data."""
address.longitude = longitude
db_session.add(address)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
@pytest.mark.parametrize('zip_code', [-1, 0, 9999, 100000])
def test_invalid_zip_code(self, address, db_session, zip_code):
"""Insert an instance with invalid data."""
address.zip_code = zip_code
db_session.add(address)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
@pytest.mark.parametrize('floor', [-1, 41])
def test_invalid_floor(self, address, db_session, floor):
"""Insert an instance with invalid data."""
address.floor = floor
db_session.add(address)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
class TestProperties:
"""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)
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)
result = address.is_primary
assert result is False

99
tests/db/test_cities.py Normal file
View file

@ -0,0 +1,99 @@
"""Test the ORM's City model."""
import pytest
from sqlalchemy.orm import exc as orm_exc
from urban_meal_delivery import db
class TestSpecialMethods:
"""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']
result = repr(city)
assert result == f'<City({name})>'
@pytest.mark.e2e
@pytest.mark.no_cover
class TestConstraints:
"""Test the database constraints defined in City."""
# pylint:disable=no-self-use
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()
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)
result = city.location
assert isinstance(result, dict)
assert len(result) == 2
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)
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']
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'])

125
tests/db/test_couriers.py Normal file
View file

@ -0,0 +1,125 @@
"""Test the ORM's Courier model."""
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."""
# 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']
result = repr(courier)
assert result == f'<Courier(#{id_})>'
@pytest.mark.e2e
@pytest.mark.no_cover
class TestConstraints:
"""Test the database constraints defined in Courier."""
# pylint:disable=no-self-use
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()
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):
"""Insert an instance with invalid data."""
courier.vehicle = 'invalid'
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
def test_negative_speed(self, courier, db_session):
"""Insert an instance with invalid data."""
courier.historic_speed = -1
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
def test_unrealistic_speed(self, courier, db_session):
"""Insert an instance with invalid data."""
courier.historic_speed = 999
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
def test_negative_capacity(self, courier, db_session):
"""Insert an instance with invalid data."""
courier.capacity = -1
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
def test_too_much_capacity(self, courier, db_session):
"""Insert an instance with invalid data."""
courier.capacity = 999
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
def test_negative_pay_per_hour(self, courier, db_session):
"""Insert an instance with invalid data."""
courier.pay_per_hour = -1
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
def test_too_much_pay_per_hour(self, courier, db_session):
"""Insert an instance with invalid data."""
courier.pay_per_hour = 9999
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
def test_negative_pay_per_order(self, courier, db_session):
"""Insert an instance with invalid data."""
courier.pay_per_order = -1
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
def test_too_much_pay_per_order(self, courier, db_session):
"""Insert an instance with invalid data."""
courier.pay_per_order = 999
db_session.add(courier)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()

51
tests/db/test_customer.py Normal file
View file

@ -0,0 +1,51 @@
"""Test the ORM's Customer model."""
import pytest
from sqlalchemy.orm import exc as orm_exc
from urban_meal_delivery import db
class TestSpecialMethods:
"""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']
result = repr(customer)
assert result == f'<Customer(#{id_})>'
@pytest.mark.e2e
@pytest.mark.no_cover
class TestConstraints:
"""Test the database constraints defined in Customer."""
# pylint:disable=no-self-use
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()

397
tests/db/test_orders.py Normal file
View file

@ -0,0 +1,397 @@
"""Test the ORM's Order model."""
import datetime
import pytest
from sqlalchemy.orm import exc as orm_exc
from urban_meal_delivery import db
class TestSpecialMethods:
"""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']
result = repr(order)
assert result == f'<Order(#{id_})>'
@pytest.mark.e2e
@pytest.mark.no_cover
class TestConstraints:
"""Test the database constraints defined in Order."""
# pylint:disable=no-self-use
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()
# TODO (order-constraints): the various Foreign Key and Check Constraints
# should be tested eventually. This is not of highest importance as
# we have a lot of confidence from the data cleaning notebook.
class TestProperties:
"""Test properties in Order."""
# 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)
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)
result = order.scheduled
assert result is True
def test_is_completed(self, order_data):
"""Test Order.completed property."""
order = db.Order(**order_data)
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)
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)
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)
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)
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)
result = order.time_to_accept
assert isinstance(result, datetime.timedelta)
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)
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)
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)
result = order.time_to_react
assert isinstance(result, datetime.timedelta)
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)
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)
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)
result = order.time_to_pickup
assert isinstance(result, datetime.timedelta)
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)
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)
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)
result = order.time_at_pickup
assert isinstance(result, datetime.timedelta)
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)
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)
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)
result = order.scheduled_pickup_at
assert isinstance(result, datetime.datetime)
def test_if_courier_early_at_pickup(self, order_data):
"""Test Order.courier_early property."""
order = db.Order(**order_data)
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)
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)
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)
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)
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)
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)
result = order.time_to_delivery
assert isinstance(result, datetime.timedelta)
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)
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)
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)
result = order.time_at_delivery
assert isinstance(result, datetime.timedelta)
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)
result = int(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)
result = int(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)
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)
with pytest.raises(AttributeError, match='scheduled'):
int(order.delivery_early)
def test_if_delivery_late_success(self, order_data):
"""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)
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)
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)
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)
result = order.total_time
assert isinstance(result, datetime.timedelta)

View file

@ -0,0 +1,80 @@
"""Test the ORM's Restaurant model."""
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 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']
result = repr(restaurant)
assert result == f'<Restaurant({name})>'
@pytest.mark.e2e
@pytest.mark.no_cover
class TestConstraints:
"""Test the database constraints defined in Restaurant."""
# pylint:disable=no-self-use
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()
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):
"""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,
),
)
def test_negative_prep_duration(self, restaurant, db_session):
"""Insert an instance with invalid data."""
restaurant.estimated_prep_duration = -1
db_session.add(restaurant)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
def test_too_high_prep_duration(self, restaurant, db_session):
"""Insert an instance with invalid data."""
restaurant.estimated_prep_duration = 2500
db_session.add(restaurant)
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()