diff --git a/CHANGES.rst b/CHANGES.rst index d04060a..6809f9e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Utils release. +0.27.4 (2014-10-23) +^^^^^^^^^^^^^^^^^^^ + +- Added assert_non_nullable, assert_nullable and assert_max_length testing methods + + 0.27.3 (2014-10-22) ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/index.rst b/docs/index.rst index da5bdd9..e43720d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,4 +20,5 @@ SQLAlchemy-Utils provides custom data types and various utility functions for SQ orm_helpers utility_classes models + testing license diff --git a/docs/testing.rst b/docs/testing.rst new file mode 100644 index 0000000..0b6a88e --- /dev/null +++ b/docs/testing.rst @@ -0,0 +1,20 @@ +Testing +======= + +.. automodule:: sqlalchemy_utils.asserts + + +assert_nullable +--------------- + +.. autofunction:: assert_nullable + +assert_non_nullable +------------------- + +.. autofunction:: assert_non_nullable + +assert_max_length +----------------- + +.. autofunction:: assert_max_length diff --git a/sqlalchemy_utils/__init__.py b/sqlalchemy_utils/__init__.py index 57d841a..3572784 100644 --- a/sqlalchemy_utils/__init__.py +++ b/sqlalchemy_utils/__init__.py @@ -1,4 +1,5 @@ from .aggregates import aggregated +from .asserts import assert_nullable, assert_non_nullable, assert_max_length from .batch import batch_fetch, with_backrefs from .decorators import generates from .exceptions import ImproperlyConfigured @@ -78,12 +79,15 @@ from .types import ( from .models import Timestamp -__version__ = '0.27.3' +__version__ = '0.27.4' __all__ = ( aggregated, analyze, + assert_max_length, + assert_non_nullable, + assert_nullable, auto_delete_orphans, batch_fetch, coercion_listener, diff --git a/sqlalchemy_utils/asserts.py b/sqlalchemy_utils/asserts.py new file mode 100644 index 0000000..85cd5fe --- /dev/null +++ b/sqlalchemy_utils/asserts.py @@ -0,0 +1,99 @@ +""" +The functions in this module can be used for testing that the constraints of +your models. Each assert function runs SQL UPDATEs that check for the existence +of given constraint. Consider the following model:: + + + class User(Base): + __tablename__ = 'user' + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String(200), nullable=True) + email = sa.Column(sa.String(255), nullable=False) + + + user = User(name='John Doe', email='john@example.com') + session.add(user) + session.commit() + + +We can easily test the constraints by assert_* functions:: + + + from sqlalchemy_utils import ( + assert_nullable, + assert_non_nullable, + assert_max_length + ) + + assert_nullable(user, 'name') + assert_non_nullable(user, 'email') + assert_max_length(user, 'name', 200) + + # raises AssertionError because the max length of email is 255 + assert_max_length(user, 'email', 300) +""" +import sqlalchemy as sa +from sqlalchemy.exc import DataError, IntegrityError + + +class raises(object): + def __init__(self, expected_exc): + self.expected_exc = expected_exc + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type != self.expected_exc: + return False + return True + + +def _update_field(obj, field, value): + session = sa.orm.object_session(obj) + table = sa.inspect(obj.__class__).columns[field].table + query = table.update().values(**{field: value}) + session.execute(query) + session.flush() + + +def assert_nullable(obj, column): + """ + Assert that given column is nullable. This is checked by running an SQL + update that assigns given column as None. + + :param obj: SQLAlchemy declarative model object + :param column: Name of the column + """ + try: + _update_field(obj, column, None) + except (IntegrityError) as e: + assert False, str(e) + + +def assert_non_nullable(obj, column): + """ + Assert that given column is not nullable. This is checked by running an SQL + update that assigns given column as None. + + :param obj: SQLAlchemy declarative model object + :param column: Name of the column + """ + with raises(IntegrityError): + _update_field(obj, column, None) + + +def assert_max_length(obj, column, max_length): + """ + Assert that the given column is of given max length. + + :param obj: SQLAlchemy declarative model object + :param column: Name of the column + """ + try: + _update_field(obj, column, u'a' * max_length) + except (DataError) as e: + assert False, str(e) + with raises(DataError): + _update_field(obj, column, u'a' * (max_length + 1)) + diff --git a/tests/test_asserts.py b/tests/test_asserts.py new file mode 100644 index 0000000..3986518 --- /dev/null +++ b/tests/test_asserts.py @@ -0,0 +1,74 @@ +import sqlalchemy as sa +import pytest +from sqlalchemy_utils import ( + assert_nullable, + assert_non_nullable, + assert_max_length +) +from sqlalchemy_utils.asserts import raises + +from tests import TestCase + + +class TestRaises(object): + def test_matching_exception(self): + with raises(Exception): + raise Exception() + assert True + + def test_non_matchin_exception(self): + with pytest.raises(Exception): + with raises(ValueError): + raise Exception() + + +class AssertionTestCase(TestCase): + dns = 'postgres://postgres@localhost/sqlalchemy_utils_test' + + def create_models(self): + class User(self.Base): + __tablename__ = 'user' + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String(20)) + age = sa.Column(sa.Integer, nullable=False) + email = sa.Column(sa.String(200), unique=True) + + self.User = User + + def setup_method(self, method): + TestCase.setup_method(self, method) + user = self.User(name='Someone', age=15) + self.session.add(user) + self.session.commit() + self.user = user + + +class TestAssertNonNullable(AssertionTestCase): + def test_non_nullable_column(self): + assert_non_nullable(self.user, 'age') + + def test_nullable_column(self): + with raises(AssertionError): + assert_non_nullable(self.user, 'name') + + +class TestAssertNullable(AssertionTestCase): + def test_nullable_column(self): + assert_nullable(self.user, 'name') + + def test_non_nullable_column(self): + with raises(AssertionError): + assert_nullable(self.user, 'age') + + +class TestAssertMaxLength(AssertionTestCase): + def test_with_max_length(self): + assert_max_length(self.user, 'name', 20) + + def test_smaller_than_max_length(self): + with raises(AssertionError): + assert_max_length(self.user, 'name', 19) + + def test_bigger_than_max_length(self): + with raises(AssertionError): + assert_max_length(self.user, 'name', 21)