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:
parent
d219fa816d
commit
fdcc93a1ea
24 changed files with 2119 additions and 4 deletions
1
tests/db/__init__.py
Normal file
1
tests/db/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Test the ORM layer."""
|
||||
244
tests/db/conftest.py
Normal file
244
tests/db/conftest.py
Normal 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
141
tests/db/test_addresses.py
Normal 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
99
tests/db/test_cities.py
Normal 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
125
tests/db/test_couriers.py
Normal 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
51
tests/db/test_customer.py
Normal 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
397
tests/db/test_orders.py
Normal 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)
|
||||
80
tests/db/test_restaurants.py
Normal file
80
tests/db/test_restaurants.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue