Add ORM models for the pixel grids
- add Grid, Pixel, and AddressPixelAssociation ORM models - each Grid belongs to a City an is characterized by the side_length of all the square Pixels contained in it - Pixels aggregate Addresses => many-to-many relationship (that is modeled with SQLAlchemy's Association Pattern to implement a couple of constraints)
This commit is contained in:
parent
6cb4be80f6
commit
f996376b13
15 changed files with 665 additions and 36 deletions
|
|
@ -85,3 +85,5 @@ courier = fake_data.courier
|
|||
customer = fake_data.customer
|
||||
order = fake_data.order
|
||||
restaurant = fake_data.restaurant
|
||||
grid = fake_data.grid
|
||||
pixel = fake_data.pixel
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
"""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
|
||||
from tests.db.fake_data.fixture_makers import make_address
|
||||
from tests.db.fake_data.fixture_makers import make_courier
|
||||
from tests.db.fake_data.fixture_makers import make_customer
|
||||
from tests.db.fake_data.fixture_makers import make_order
|
||||
from tests.db.fake_data.fixture_makers import make_restaurant
|
||||
from tests.db.fake_data.static_fixtures import address
|
||||
from tests.db.fake_data.static_fixtures import city
|
||||
from tests.db.fake_data.static_fixtures import city_data
|
||||
from tests.db.fake_data.static_fixtures import courier
|
||||
from tests.db.fake_data.static_fixtures import customer
|
||||
from tests.db.fake_data.static_fixtures import grid
|
||||
from tests.db.fake_data.static_fixtures import order
|
||||
from tests.db.fake_data.static_fixtures import pixel
|
||||
from tests.db.fake_data.static_fixtures import restaurant
|
||||
|
|
|
|||
|
|
@ -56,3 +56,15 @@ def restaurant(address, make_restaurant):
|
|||
def order(make_order, restaurant):
|
||||
"""An `Order` object for the `restaurant`."""
|
||||
return make_order(restaurant=restaurant)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def grid(city):
|
||||
"""A `Grid` with a pixel area of 1 square kilometer."""
|
||||
return db.Grid(city=city, side_length=1000)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pixel(grid):
|
||||
"""The `Pixel` in the lower-left corner of the `grid`."""
|
||||
return db.Pixel(grid=grid, n_x=0, n_y=0)
|
||||
|
|
|
|||
136
tests/db/test_addresses_pixels.py
Normal file
136
tests/db/test_addresses_pixels.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
"""Test the ORM's `AddressPixelAssociation` model.
|
||||
|
||||
Implementation notes:
|
||||
The test suite has 100% coverage without the test cases in this module.
|
||||
That is so as the `AddressPixelAssociation` model is imported into the
|
||||
`urban_meal_delivery.db` namespace so that the `Address` and `Pixel` models
|
||||
can find it upon initialization. Yet, none of the other unit tests run any
|
||||
code associated with it. Therefore, we test it here as non-e2e tests and do
|
||||
not measure its coverage.
|
||||
"""
|
||||
# pylint:disable=no-self-use
|
||||
|
||||
import pytest
|
||||
import sqlalchemy as sqla
|
||||
from sqlalchemy import exc as sa_exc
|
||||
|
||||
from urban_meal_delivery import db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def assoc(address, pixel):
|
||||
"""An association between `address` and `pixel`."""
|
||||
return db.AddressPixelAssociation(address=address, pixel=pixel)
|
||||
|
||||
|
||||
@pytest.mark.no_cover
|
||||
class TestSpecialMethods:
|
||||
"""Test special methods in `Pixel`."""
|
||||
|
||||
def test_create_an_address_pixel_association(self, assoc):
|
||||
"""Test instantiation of a new `AddressPixelAssociation` object."""
|
||||
assert assoc is not None
|
||||
|
||||
|
||||
@pytest.mark.db
|
||||
@pytest.mark.no_cover
|
||||
class TestConstraints:
|
||||
"""Test the database constraints defined in `AddressPixelAssociation`.
|
||||
|
||||
The foreign keys to `City` and `Grid` are tested via INSERT and not
|
||||
DELETE statements as the latter would already fail because of foreign
|
||||
keys defined in `Address` and `Pixel`.
|
||||
"""
|
||||
|
||||
def test_insert_into_database(self, db_session, assoc):
|
||||
"""Insert an instance into the (empty) database."""
|
||||
assert db_session.query(db.AddressPixelAssociation).count() == 0
|
||||
|
||||
db_session.add(assoc)
|
||||
db_session.commit()
|
||||
|
||||
assert db_session.query(db.AddressPixelAssociation).count() == 1
|
||||
|
||||
def test_delete_a_referenced_address(self, db_session, assoc):
|
||||
"""Remove a record that is referenced with a FK."""
|
||||
db_session.add(assoc)
|
||||
db_session.commit()
|
||||
|
||||
# Must delete without ORM as otherwise an UPDATE statement is emitted.
|
||||
stmt = sqla.delete(db.Address).where(db.Address.id == assoc.address.id)
|
||||
|
||||
with pytest.raises(
|
||||
sa_exc.IntegrityError,
|
||||
match='fk_addresses_pixels_to_addresses_via_address_id_city_id',
|
||||
):
|
||||
db_session.execute(stmt)
|
||||
|
||||
def test_reference_an_invalid_city(self, db_session, address, pixel):
|
||||
"""Insert a record with an invalid foreign key."""
|
||||
db_session.add(address)
|
||||
db_session.add(pixel)
|
||||
db_session.commit()
|
||||
|
||||
# Must insert without ORM as otherwise SQLAlchemy figures out
|
||||
# that something is wrong before any query is sent to the database.
|
||||
stmt = sqla.insert(db.AddressPixelAssociation).values(
|
||||
address_id=address.id,
|
||||
city_id=999,
|
||||
grid_id=pixel.grid.id,
|
||||
pixel_id=pixel.id,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
sa_exc.IntegrityError,
|
||||
match='fk_addresses_pixels_to_addresses_via_address_id_city_id',
|
||||
):
|
||||
db_session.execute(stmt)
|
||||
|
||||
def test_reference_an_invalid_grid(self, db_session, address, pixel):
|
||||
"""Insert a record with an invalid foreign key."""
|
||||
db_session.add(address)
|
||||
db_session.add(pixel)
|
||||
db_session.commit()
|
||||
|
||||
# Must insert without ORM as otherwise SQLAlchemy figures out
|
||||
# that something is wrong before any query is sent to the database.
|
||||
stmt = sqla.insert(db.AddressPixelAssociation).values(
|
||||
address_id=address.id,
|
||||
city_id=address.city.id,
|
||||
grid_id=999,
|
||||
pixel_id=pixel.id,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
sa_exc.IntegrityError,
|
||||
match='fk_addresses_pixels_to_grids_via_grid_id_city_id',
|
||||
):
|
||||
db_session.execute(stmt)
|
||||
|
||||
def test_delete_a_referenced_pixel(self, db_session, assoc):
|
||||
"""Remove a record that is referenced with a FK."""
|
||||
db_session.add(assoc)
|
||||
db_session.commit()
|
||||
|
||||
# Must delete without ORM as otherwise an UPDATE statement is emitted.
|
||||
stmt = sqla.delete(db.Pixel).where(db.Pixel.id == assoc.pixel.id)
|
||||
|
||||
with pytest.raises(
|
||||
sa_exc.IntegrityError,
|
||||
match='fk_addresses_pixels_to_pixels_via_pixel_id_grid_id',
|
||||
):
|
||||
db_session.execute(stmt)
|
||||
|
||||
def test_put_an_address_on_a_grid_twice(self, db_session, address, assoc, pixel):
|
||||
"""Insert a record that violates a unique constraint."""
|
||||
db_session.add(assoc)
|
||||
db_session.commit()
|
||||
|
||||
# Create a neighboring `Pixel` and put the same `address` as in `pixel` in it.
|
||||
neighbor = db.Pixel(grid=pixel.grid, n_x=pixel.n_x, n_y=pixel.n_y + 1)
|
||||
another_assoc = db.AddressPixelAssociation(address=address, pixel=neighbor)
|
||||
|
||||
db_session.add(another_assoc)
|
||||
|
||||
with pytest.raises(sa_exc.IntegrityError, match='duplicate key value'):
|
||||
db_session.commit()
|
||||
60
tests/db/test_grids.py
Normal file
60
tests/db/test_grids.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""Test the ORM's `Grid` model."""
|
||||
# pylint:disable=no-self-use
|
||||
|
||||
import pytest
|
||||
import sqlalchemy as sqla
|
||||
from sqlalchemy import exc as sa_exc
|
||||
|
||||
from urban_meal_delivery import db
|
||||
|
||||
|
||||
class TestSpecialMethods:
|
||||
"""Test special methods in `Grid`."""
|
||||
|
||||
def test_create_grid(self, grid):
|
||||
"""Test instantiation of a new `Grid` object."""
|
||||
assert grid is not None
|
||||
|
||||
def test_text_representation(self, grid):
|
||||
"""`Grid` has a non-literal text representation."""
|
||||
result = repr(grid)
|
||||
|
||||
assert result == f'<Grid: {grid.pixel_area}>'
|
||||
|
||||
|
||||
@pytest.mark.db
|
||||
@pytest.mark.no_cover
|
||||
class TestConstraints:
|
||||
"""Test the database constraints defined in `Grid`."""
|
||||
|
||||
def test_insert_into_database(self, db_session, grid):
|
||||
"""Insert an instance into the (empty) database."""
|
||||
assert db_session.query(db.Grid).count() == 0
|
||||
|
||||
db_session.add(grid)
|
||||
db_session.commit()
|
||||
|
||||
assert db_session.query(db.Grid).count() == 1
|
||||
|
||||
def test_delete_a_referenced_city(self, db_session, grid):
|
||||
"""Remove a record that is referenced with a FK."""
|
||||
db_session.add(grid)
|
||||
db_session.commit()
|
||||
|
||||
# Must delete without ORM as otherwise an UPDATE statement is emitted.
|
||||
stmt = sqla.delete(db.City).where(db.City.id == grid.city.id)
|
||||
|
||||
with pytest.raises(
|
||||
sa_exc.IntegrityError, match='fk_grids_to_cities_via_city_id',
|
||||
):
|
||||
db_session.execute(stmt)
|
||||
|
||||
|
||||
class TestProperties:
|
||||
"""Test properties in `Grid`."""
|
||||
|
||||
def test_pixel_area(self, grid):
|
||||
"""Test `Grid.pixel_area` property."""
|
||||
result = grid.pixel_area
|
||||
|
||||
assert result == 1.0
|
||||
90
tests/db/test_pixels.py
Normal file
90
tests/db/test_pixels.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""Test the ORM's `Pixel` model."""
|
||||
# pylint:disable=no-self-use
|
||||
|
||||
import pytest
|
||||
import sqlalchemy as sqla
|
||||
from sqlalchemy import exc as sa_exc
|
||||
|
||||
from urban_meal_delivery import db
|
||||
|
||||
|
||||
class TestSpecialMethods:
|
||||
"""Test special methods in `Pixel`."""
|
||||
|
||||
def test_create_pixel(self, pixel):
|
||||
"""Test instantiation of a new `Pixel` object."""
|
||||
assert pixel is not None
|
||||
|
||||
def test_text_representation(self, pixel):
|
||||
"""`Pixel` has a non-literal text representation."""
|
||||
result = repr(pixel)
|
||||
|
||||
assert result == f'<Pixel: ({pixel.n_x}, {pixel.n_y})>'
|
||||
|
||||
|
||||
@pytest.mark.db
|
||||
@pytest.mark.no_cover
|
||||
class TestConstraints:
|
||||
"""Test the database constraints defined in `Pixel`."""
|
||||
|
||||
def test_insert_into_database(self, db_session, pixel):
|
||||
"""Insert an instance into the (empty) database."""
|
||||
assert db_session.query(db.Pixel).count() == 0
|
||||
|
||||
db_session.add(pixel)
|
||||
db_session.commit()
|
||||
|
||||
assert db_session.query(db.Pixel).count() == 1
|
||||
|
||||
def test_delete_a_referenced_grid(self, db_session, pixel):
|
||||
"""Remove a record that is referenced with a FK."""
|
||||
db_session.add(pixel)
|
||||
db_session.commit()
|
||||
|
||||
# Must delete without ORM as otherwise an UPDATE statement is emitted.
|
||||
stmt = sqla.delete(db.Grid).where(db.Grid.id == pixel.grid.id)
|
||||
|
||||
with pytest.raises(
|
||||
sa_exc.IntegrityError, match='fk_pixels_to_grids_via_grid_id',
|
||||
):
|
||||
db_session.execute(stmt)
|
||||
|
||||
def test_negative_n_x(self, db_session, pixel):
|
||||
"""Insert an instance with invalid data."""
|
||||
pixel.n_x = -1
|
||||
db_session.add(pixel)
|
||||
|
||||
with pytest.raises(sa_exc.IntegrityError, match='n_x_is_positive'):
|
||||
db_session.commit()
|
||||
|
||||
def test_negative_n_y(self, db_session, pixel):
|
||||
"""Insert an instance with invalid data."""
|
||||
pixel.n_y = -1
|
||||
db_session.add(pixel)
|
||||
|
||||
with pytest.raises(sa_exc.IntegrityError, match='n_y_is_positive'):
|
||||
db_session.commit()
|
||||
|
||||
def test_non_unique_coordinates_within_a_grid(self, db_session, pixel):
|
||||
"""Insert an instance with invalid data."""
|
||||
another_pixel = db.Pixel(grid=pixel.grid, n_x=pixel.n_x, n_y=pixel.n_y)
|
||||
db_session.add(another_pixel)
|
||||
|
||||
with pytest.raises(sa_exc.IntegrityError, match='duplicate key value'):
|
||||
db_session.commit()
|
||||
|
||||
|
||||
class TestProperties:
|
||||
"""Test properties in `Pixel`."""
|
||||
|
||||
def test_side_length(self, pixel):
|
||||
"""Test `Pixel.side_length` property."""
|
||||
result = pixel.side_length
|
||||
|
||||
assert result == 1_000
|
||||
|
||||
def test_area(self, pixel):
|
||||
"""Test `Pixel.area` property."""
|
||||
result = pixel.area
|
||||
|
||||
assert result == 1.0
|
||||
Loading…
Add table
Add a link
Reference in a new issue