2
votes

I have flask application (RESTful API backend) that uses flask-sqlalchemy lib. The integration test uses Pytest with fixtures, some create records for test purposes. The problem is that when testing scenario that's expected to raise unique constraint failure at the database level, the records created for test via fixtures all got rolled back, causing other tests to fail with error sqlalchemy.exc.InvalidRequestError: This Session's transaction has been rolled back due to a previous exception during flush. To begin a new transaction with this Session, first issue Session.rollback(). Original exception was: (sqlite3.IntegrityError) UNIQUE constraint failed: my_table.field1.

How do I go about creating distinct SQLAlchemy session for testing so that test records can be committed to DB and not impacted by errors that happen inside Flask request lifecycle?

### globals.py ###

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
### app.py ###

from sqlalchemy.exc import IntegrityError
from .globals import db

def handle_unique_constraint(error):
    return {"msg": "Duplicate"}, 409

def create_app(connection_string):
    app = Flask(__name__)
    app.register_error_handler(IntegrityError, handle_unique_constraint)
    app.config['SQLALCHEMY_DATABASE_URI'] = connection_string

    # register API blueprint ...
    # e.g. create new user record, done like this:
    #
    # db.session.add(User(**{'email': path_param}))
    # db.session.commit()

    db.init_app(app)
    return app
### conftest.py ###

from my_package.app import create_app
from my_package.globals import db as app_db
from my_package.models.user import User

@fixture(scope='session')
def app(request):
    app = create_app('https://localhost')
    app.debug = True

    with app.app_context():
        yield app

@fixture(scope='session')
def db(app):
    app_db.create_all()
    return app_db

@fixture(scope='session')
def client(app):

    with app.test_client() as client:
        yield client

@fixture(scope='function')
def test_user(db):
    user = User(**{'email': generate_random()})
    db.session.add(user)
    db.session.commit()
    db.session.refresh(user)
### test_user.py ###



# This test passses

def test_repeat_email(client, test_user):
    resp = client.post('/users/emails/{}'.format(test_user.email))
    assert resp.status_code == 409


# This test errors out during setting up test_user fixture
# with aforementioned error message

def test_good_email(client, test_user): # <- this 
    resp = client.post('/users/emails/{}'.format('[email protected]'))
    assert resp.status_code == 201
1

1 Answers

0
votes

You have to implement a setUp and a tearDown.

When running a tests the setUp will be run at the start of each test. The tearDown will be run at the end of each one.

In the setUp you will: initialize the database

In the tearnDown you will: close db connections, remove items created ...


I'm not familiar to pytest. According to this and this.

Before the yield it's the setUp part and after it's the tearDown.

You should have something like this :

@fixture(scope='session')
def app(request):
    app = create_app('https://localhost')
    app.debug = True
    
    ctx = app.app_context()
    ctx.push()

    yield app

    ctx.pop()


@fixture(scope='session')
def db(app):
    app_db.create_all()
    yield app_db
    app_db.drop_all()