Add Tests
This commit is contained in:
5
tests/__init__.py
Normal file
5
tests/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Sigl Test Package.
|
||||
|
||||
Simple Grocery List (Sigl) | sigl.app
|
||||
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||
"""
|
||||
137
tests/conftest.py
Normal file
137
tests/conftest.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Sigl Test Fixtures.
|
||||
|
||||
Simple Grocery List (Sigl) | sigl.app
|
||||
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from alembic.command import upgrade
|
||||
from alembic.config import Config
|
||||
import flask
|
||||
import pytest
|
||||
|
||||
import sigl
|
||||
from sigl.database import db as _db
|
||||
from sigl.factory import create_app
|
||||
|
||||
DATA_DIR = '.pytest-data'
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def app_config(request):
|
||||
"""Application Configuration for the Test Session.
|
||||
|
||||
Loads configuration from config/test.py, patches the database file with
|
||||
a session-global temporary file, and returns the new configuration data
|
||||
to pass to create_app().
|
||||
"""
|
||||
test_dir = os.path.dirname(__file__)
|
||||
test_cfg = os.path.join(test_dir, '../config/test.py')
|
||||
cfg_file = os.environ.get('SIGL_TEST_CONFIG', test_cfg)
|
||||
cfg_data = flask.Config(test_dir)
|
||||
cfg_data.from_pyfile(cfg_file)
|
||||
|
||||
# Patch Test Configuration
|
||||
cfg_data['TESTING'] = True
|
||||
cfg_data['DB_DRIVER'] = 'sqlite'
|
||||
cfg_data['DB_FILE'] = os.path.join(test_dir, DATA_DIR, 'test.db')
|
||||
|
||||
# Ensure Database Path exists
|
||||
db_dir = os.path.dirname(cfg_data['DB_FILE'])
|
||||
if not os.path.isdir(db_dir):
|
||||
os.mkdir(db_dir)
|
||||
if not os.path.isdir(db_dir):
|
||||
raise Exception('Database path "%s" does not exist' % (db_dir))
|
||||
|
||||
db_file = cfg_data['DB_FILE']
|
||||
if os.path.exists(db_file):
|
||||
os.unlink(db_file)
|
||||
|
||||
def teardown():
|
||||
if os.path.exists(db_file):
|
||||
os.unlink(db_file)
|
||||
if os.path.exists(db_dir) and len(os.listdir(db_dir)) == 0:
|
||||
os.rmdir(db_dir)
|
||||
|
||||
request.addfinalizer(teardown)
|
||||
return cfg_data
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def app(request, app_config):
|
||||
"""Module-Wide Sigl Flask Application with Database.
|
||||
|
||||
Loads configuration from config/test.py, patches the database file with
|
||||
a session-global temporary file, and initializes the application.
|
||||
"""
|
||||
# Apply Database Migrations
|
||||
_db.clear_mappers()
|
||||
_app = create_app(app_config, __name__)
|
||||
with _app.app_context():
|
||||
alembic_config = Config('migrations/alembic.ini')
|
||||
alembic_config.set_main_option('script_location', 'migrations')
|
||||
upgrade(alembic_config, 'head')
|
||||
_db.clear_mappers()
|
||||
|
||||
# Initialize Application and Context
|
||||
app = create_app(app_config, __name__)
|
||||
ctx = app.app_context()
|
||||
ctx.push()
|
||||
|
||||
# Add Finalizers
|
||||
def teardown():
|
||||
_db.drop_all()
|
||||
_db.clear_mappers()
|
||||
if os.path.exists(app.config['DB_FILE']):
|
||||
os.unlink(app.config['DB_FILE'])
|
||||
ctx.pop()
|
||||
|
||||
request.addfinalizer(teardown)
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def app_without_database(request, app_config):
|
||||
"""Module-Wide Sigl Flask Application without Database.
|
||||
|
||||
Loads configuration from config/test.py and initializes the application,
|
||||
but does not create the database.
|
||||
"""
|
||||
_db.clear_mappers()
|
||||
|
||||
# Initialize Application and Context
|
||||
app = create_app(app_config, __name__)
|
||||
ctx = app.app_context()
|
||||
ctx.push()
|
||||
|
||||
# Add Finalizers
|
||||
def teardown():
|
||||
_db.clear_mappers()
|
||||
_db.drop_all()
|
||||
ctx.pop()
|
||||
|
||||
request.addfinalizer(teardown)
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def session(request, monkeypatch, app):
|
||||
"""Create a new Database Session for the Request."""
|
||||
with app.app_context():
|
||||
connection = _db.engine.connect()
|
||||
transaction = connection.begin()
|
||||
|
||||
options = dict(bind=connection, binds={})
|
||||
session = _db.create_scoped_session(options=options)
|
||||
|
||||
# Patch sigl.db with the current session
|
||||
monkeypatch.setattr(_db, 'session', session)
|
||||
|
||||
def teardown():
|
||||
transaction.rollback()
|
||||
connection.close()
|
||||
session.remove()
|
||||
|
||||
request.addfinalizer(teardown)
|
||||
return session
|
||||
180
tests/test_01_db_makeuri.py
Normal file
180
tests/test_01_db_makeuri.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""Test Database URI Helper.
|
||||
|
||||
Simple Grocery List (Sigl) | sigl.app
|
||||
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||
"""
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from sigl.database.util import make_uri
|
||||
from sigl.exc import ConfigError
|
||||
|
||||
BASE_TEST_CONFIG = {
|
||||
'APP_SESSION_KEY': 'test',
|
||||
'APP_TOKEN_KEY': 'test',
|
||||
'APP_TOKEN_SALT': 'test',
|
||||
'DB_DRIVER': 'sqlite',
|
||||
'DB_FILE': '/dev/null',
|
||||
'MAIL_SERVER': 'localhost',
|
||||
'MAIL_SENDER': 'test@localhost',
|
||||
}
|
||||
|
||||
|
||||
class MockApp(object):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
|
||||
def test_dbconfig_no_driver():
|
||||
'''
|
||||
Application startup should fail if DB_DRIVER is not in the configuration
|
||||
'''
|
||||
test_cfg = dict(BASE_TEST_CONFIG)
|
||||
del test_cfg['DB_DRIVER']
|
||||
|
||||
app = MockApp(test_cfg)
|
||||
with pytest.raises(ConfigError, match='application configuration') as excinfo:
|
||||
make_uri(app)
|
||||
|
||||
# Ensure that the offending key is DB_DRIVER
|
||||
assert excinfo.value.config_key == 'DB_DRIVER'
|
||||
|
||||
|
||||
def test_dbconfig_sqlite_no_file():
|
||||
'''
|
||||
Application Startup should fail if an SQlite database is specified
|
||||
without a DB_FILE key
|
||||
'''
|
||||
test_cfg = dict(BASE_TEST_CONFIG)
|
||||
del test_cfg['DB_FILE']
|
||||
|
||||
app = MockApp(test_cfg)
|
||||
with pytest.raises(ConfigError, match='application configuration') as excinfo:
|
||||
make_uri(app)
|
||||
|
||||
# Ensure that the offending key is DB_FILE
|
||||
assert excinfo.value.config_key == 'DB_FILE'
|
||||
|
||||
|
||||
def test_dbconfig_sqlite_with_file():
|
||||
'''
|
||||
Application Startup should succeed if an SQlite database is specified
|
||||
with a DB_FILE key
|
||||
'''
|
||||
test_cfg = dict(BASE_TEST_CONFIG)
|
||||
app = MockApp(test_cfg)
|
||||
uri = make_uri(app)
|
||||
|
||||
# Ensure that the URI was correctly configured and logged
|
||||
assert uri == 'sqlite:////dev/null'
|
||||
|
||||
|
||||
def test_dbconfig_mysql_no_host():
|
||||
'''
|
||||
Application Startup should fail if a MySQL database is specified
|
||||
without a DB_HOST key
|
||||
'''
|
||||
test_cfg = dict(BASE_TEST_CONFIG)
|
||||
test_cfg['DB_DRIVER'] = 'mysql'
|
||||
app = MockApp(test_cfg)
|
||||
|
||||
with pytest.raises(ConfigError, match='application configuration') as excinfo:
|
||||
make_uri(app)
|
||||
|
||||
# Ensure that the offending key is DB_FILE
|
||||
assert excinfo.value.config_key == 'DB_HOST'
|
||||
|
||||
|
||||
def test_dbconfig_mysql_no_name():
|
||||
'''
|
||||
Application Startup should fail if a MySQL database is specified
|
||||
without a DB_HOST key
|
||||
'''
|
||||
test_cfg = dict(BASE_TEST_CONFIG)
|
||||
test_cfg['DB_DRIVER'] = 'mysql'
|
||||
test_cfg['DB_HOST'] = 'localhost'
|
||||
app = MockApp(test_cfg)
|
||||
|
||||
with pytest.raises(ConfigError, match='application configuration') as excinfo:
|
||||
make_uri(app)
|
||||
|
||||
# Ensure that the offending key is DB_NAME
|
||||
assert excinfo.value.config_key == 'DB_NAME'
|
||||
|
||||
|
||||
def test_dbconfig_mysql_no_username():
|
||||
'''
|
||||
Application Startup should fail if a MySQL database is specified
|
||||
with a DB_USERNAME key but without a DB_PASSWORD key
|
||||
'''
|
||||
test_cfg = dict(BASE_TEST_CONFIG)
|
||||
test_cfg['DB_DRIVER'] = 'mysql'
|
||||
test_cfg['DB_HOST'] = 'localhost'
|
||||
test_cfg['DB_NAME'] = 'test'
|
||||
test_cfg['DB_PASSWORD'] = 'hunter2'
|
||||
app = MockApp(test_cfg)
|
||||
|
||||
with pytest.raises(ConfigError, match='application configuration') as excinfo:
|
||||
make_uri(app)
|
||||
|
||||
# Ensure that the offending key is DB_USERNAME
|
||||
assert excinfo.value.config_key == 'DB_USERNAME'
|
||||
|
||||
|
||||
def test_dbconfig_mysql_without_port():
|
||||
'''
|
||||
Application Startup should succeed when a MySQL database is specified
|
||||
with a DB_PORT key
|
||||
'''
|
||||
test_cfg = dict(BASE_TEST_CONFIG)
|
||||
test_cfg['DB_DRIVER'] = 'mysql'
|
||||
test_cfg['DB_HOST'] = 'localhost'
|
||||
test_cfg['DB_NAME'] = 'test'
|
||||
test_cfg['DB_USERNAME'] = 'root'
|
||||
test_cfg['DB_PASSWORD'] = 'hunter^2'
|
||||
|
||||
app = MockApp(test_cfg)
|
||||
uri = make_uri(app)
|
||||
|
||||
# Ensure that the application startup succeeded
|
||||
assert uri == 'mysql://root:hunter%5E2@localhost/test'
|
||||
|
||||
|
||||
def test_dbconfig_mysql_with_port():
|
||||
'''
|
||||
Application Startup should succeed when a MySQL database is specified
|
||||
with a DB_PORT key
|
||||
'''
|
||||
test_cfg = dict(BASE_TEST_CONFIG)
|
||||
test_cfg['DB_DRIVER'] = 'mysql'
|
||||
test_cfg['DB_HOST'] = 'localhost'
|
||||
test_cfg['DB_PORT'] = 1234
|
||||
test_cfg['DB_NAME'] = 'test'
|
||||
test_cfg['DB_USERNAME'] = 'root'
|
||||
test_cfg['DB_PASSWORD'] = 'hunter^2'
|
||||
|
||||
app = MockApp(test_cfg)
|
||||
uri = make_uri(app)
|
||||
|
||||
# Ensure that the application startup succeeded
|
||||
assert uri == 'mysql://root:hunter%5E2@localhost:1234/test'
|
||||
|
||||
|
||||
def test_dbconfig_mysql_with_username():
|
||||
'''
|
||||
Application Startup should succeed when a MySQL database is specified
|
||||
with a DB_USERNAME key and without a DB_PASSWORD key
|
||||
'''
|
||||
test_cfg = dict(BASE_TEST_CONFIG)
|
||||
test_cfg['DB_DRIVER'] = 'mysql'
|
||||
test_cfg['DB_HOST'] = 'localhost'
|
||||
test_cfg['DB_PORT'] = 1234
|
||||
test_cfg['DB_NAME'] = 'test'
|
||||
test_cfg['DB_USERNAME'] = 'root'
|
||||
|
||||
app = MockApp(test_cfg)
|
||||
uri = make_uri(app)
|
||||
|
||||
# Ensure that the application startup succeeded
|
||||
assert uri == 'mysql://root@localhost:1234/test'
|
||||
78
tests/test_11_product_model.py
Normal file
78
tests/test_11_product_model.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Test the Product and ProductLocation Models.
|
||||
|
||||
Simple Grocery List (Sigl) | sigl.app
|
||||
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from sigl.domain.models import Product, ProductLocation
|
||||
|
||||
# Always use 'app' fixture so ORM gets initialized
|
||||
pytestmark = pytest.mark.usefixtures('app')
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_product_model_init(session):
|
||||
"""Test newly created Products have no Locations."""
|
||||
p = Product(name='Eggs', category='Dairy')
|
||||
|
||||
session.add(p)
|
||||
session.commit()
|
||||
|
||||
assert p.id is not None
|
||||
assert p.name == 'Eggs'
|
||||
assert p.category == 'Dairy'
|
||||
assert not p.locations
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_product_model_can_add_location(session):
|
||||
"""Test that a Location can be added to a Product."""
|
||||
p = Product(name='Eggs', category='Dairy')
|
||||
l = ProductLocation(product=p, store='Pavilions', aisle='Back Wall')
|
||||
|
||||
session.add(p)
|
||||
session.add(l)
|
||||
session.commit()
|
||||
|
||||
assert l.aisle == 'Back Wall'
|
||||
assert l.bin is None
|
||||
assert l.product == p
|
||||
|
||||
assert l in p.locations
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_product_model_can_add_multiple_stores(session):
|
||||
"""Test that multiple Locations can be added to a Product."""
|
||||
p = Product(name='Eggs', category='Dairy')
|
||||
l1 = ProductLocation(product=p, store='Pavilions', aisle='Back Wall')
|
||||
l2 = ProductLocation(product=p, store='Stater Brothers', aisle='Left Wall')
|
||||
|
||||
session.add(p)
|
||||
session.add(l1)
|
||||
session.add(l2)
|
||||
session.commit()
|
||||
|
||||
assert l1.product == p
|
||||
assert l1 in p.locations
|
||||
|
||||
assert l2.product == p
|
||||
assert l2 in p.locations
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_product_model_same_store_fails(session):
|
||||
"""Test that two Locations with the same Store cannot be added to a Product."""
|
||||
p = Product(name='Eggs', category='Dairy')
|
||||
l1 = ProductLocation(product=p, store='Pavilions', aisle='Back Wall')
|
||||
l2 = ProductLocation(product=p, store='Pavilions', aisle='Left Wall')
|
||||
|
||||
session.add(p)
|
||||
session.add(l1)
|
||||
session.add(l2)
|
||||
|
||||
with pytest.raises(IntegrityError):
|
||||
session.commit()
|
||||
11
tests/tox.ini
Normal file
11
tests/tox.ini
Normal file
@@ -0,0 +1,11 @@
|
||||
[flake8]
|
||||
ignore = D300,D400,E201,E202,E203,E241,E501,E712
|
||||
|
||||
[pytest]
|
||||
addopts = --strict-markers
|
||||
markers =
|
||||
spike: marks test as "spikes" that are intended to develop algorithms but not test behavior (deselected by default, select with '-m spike')
|
||||
unit: marks tests as domain-level unit tests (deselect with '-m "not unit"')
|
||||
wip: marks tests as works in progress and likely to fail (deselect with '-m "not wip"')
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning:eventlet.*
|
||||
Reference in New Issue
Block a user