1
votes

I started developing a Flask app with TDD.

The production code as well as the tests are both using the same generic SQLAlchemy instance with SQlite, for the benefits of re usability.

In __init__.py :

db = SQLAlchemy()

Therefore, both the tests and the real application need to access this db instance, but with a different configurations: tests.py uses a /tmp/ file to create dummy data as it goes.

Hence I take care of calling the right app context everytime I use 'db', regarding the Flask_SQLAlchemy documentation.

Structure:

tests.py
run.py
application/
    __init__.py
    config.py
    models.py
    views.py

Here is some key code:

__init.py__ :

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

from application.models import *

def create_app(conf_object):
    app = Flask(__name__)
    app.config.from_object(conf_object)
    db.init_app(app)
    return (app, db)

tests.py :

from flask_sqlalchemy import SQLAlchemy
from application import create_app
import unittest
import os


app, db = create_app('application.config.TestConf') # Test conf: db file will bt a tempfile
from application.models import Update, Update_State, Device, Confirmation
db.create_all(app=app) # Create tables with app context


class APITestCase(unittest.TestCase):

    def setUp(self):
        """Sets up a hardcoded dummy-data database before each test"""

        # Hardcoded fake database

        d1 = Device('1223456789', 'hub', 'R3pTkEAHyUKbUJW7bSK8k5')
        d2 = Device('AZERTY', 'hub', 'R3pTkEAHyUKbUJW7bSK8k5')

        db.session.add(d1)
        db.session.add(d2)

        db.session.commit()

        self.app = app.test_client()

    def tearDown(self):
        """Destroy fake db after each test"""
        os.close(app.config['DB_FD'])
        os.unlink(app.config['DB_FILE'])


    # assert functions

    def test_confirmation_msg_correct(self):
        tester = app.test_client(self)
        response = tester.get('/confirmation-msg', query_string=dict(deviceid='1223456789', updateid='R3pTkEAHyUKbUJW7bSK8k5'), content_type='html/text')
        self.assertEqual(response.status_code, 200)



if __name__ == '__main__':
    with app.app_context(): # Because using using a generic SQLAlchemy() 'db' instance, we need to specify the app. See Flask_sqlalchemy doc, 'Introduction into contexts'
        unittest.main()

models.py :

from application import db
import datetime
import shortuuid

class Device(db.Model):
    id = db.Column(db.String(255), primary_key=True)
    type = db.Column(db.String(255))
    version_id = db.Column(db.String(511))

    def __init__(self, id, type, version_id):
        self.id = id
        self.type = type
        self.version_id = version_id

There are some other models, but I keep it simple for the explanation.

The problem is in the setUp(self) function, where dummy data is added to the database. The engine can't find the tables even though the models are imported and the code is exectuted in the correct app context.

no such table: device

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.OperationalError) no such table: device [SQL: 'INSERT INTO device (id, type, version_id) VALUES (?, ?, ?)'] [parameters: (('1223456789', 'hub', 'R3pTkEAHyUKbUJW7bSK8k5'), ('AZERTY', 'hub', 'R3pTkEAHyUKbUJW7bSK8k5'))] (Background on this error at: http://sqlalche.me/e/e3q8)

Moreover, the tables are apparently well created with db.create_all(app=app), when setting SQLALCHEMY_ECHO = True :

2018-07-31 17:06:45,333 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
2018-07-31 17:06:45,333 INFO sqlalchemy.engine.base.Engine ()
2018-07-31 17:06:45,334 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
2018-07-31 17:06:45,334 INFO sqlalchemy.engine.base.Engine ()
2018-07-31 17:06:45,334 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("update__state")
2018-07-31 17:06:45,334 INFO sqlalchemy.engine.base.Engine ()
2018-07-31 17:06:45,335 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("device")
2018-07-31 17:06:45,335 INFO sqlalchemy.engine.base.Engine ()
2018-07-31 17:06:45,335 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("confirmation")
2018-07-31 17:06:45,335 INFO sqlalchemy.engine.base.Engine ()
2018-07-31 17:06:45,335 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("update")
2018-07-31 17:06:45,336 INFO sqlalchemy.engine.base.Engine ()
2018-07-31 17:06:45,336 INFO sqlalchemy.engine.base.Engine 
CREATE TABLE "update" (
    update_id VARCHAR(511) NOT NULL, 
    version VARCHAR(255), 
    description VARCHAR(2047), 
    date_created DATETIME, 
    PRIMARY KEY (update_id)
)


2018-07-31 17:06:45,336 INFO sqlalchemy.engine.base.Engine ()
2018-07-31 17:06:45,349 INFO sqlalchemy.engine.base.Engine COMMIT
2018-07-31 17:06:45,350 INFO sqlalchemy.engine.base.Engine 
CREATE TABLE update__state (
    last_communicated_version_id VARCHAR(255) NOT NULL, 
    PRIMARY KEY (last_communicated_version_id), 
    FOREIGN KEY(last_communicated_version_id) REFERENCES "update" (update_id)
)


2018-07-31 17:06:45,350 INFO sqlalchemy.engine.base.Engine ()
2018-07-31 17:06:45,368 INFO sqlalchemy.engine.base.Engine COMMIT
2018-07-31 17:06:45,376 INFO sqlalchemy.engine.base.Engine 
CREATE TABLE device (
    id VARCHAR(255) NOT NULL, 
    type VARCHAR(255), 
    version_id VARCHAR(511) NOT NULL, 
    PRIMARY KEY (id), 
    FOREIGN KEY(version_id) REFERENCES "update" (update_id)
)


2018-07-31 17:06:45,377 INFO sqlalchemy.engine.base.Engine ()
2018-07-31 17:06:45,387 INFO sqlalchemy.engine.base.Engine COMMIT
2018-07-31 17:06:45,388 INFO sqlalchemy.engine.base.Engine 
CREATE TABLE confirmation (
    id INTEGER NOT NULL, 
    device_id INTEGER, 
    version_id VARCHAR(511) NOT NULL, 
    timestamp DATETIME, 
    PRIMARY KEY (id), 
    UNIQUE (device_id), 
    FOREIGN KEY(device_id) REFERENCES device (id), 
    FOREIGN KEY(version_id) REFERENCES "update" (update_id)
)

Hope I am clear, thanks in advance.

1
When do you think db.createall(app) gets called?mad_
In tests.py. I removed the create_all() from the init.py, and checked it by debugging. Just edited the postGuilhem Fry
Still the same errorGuilhem Fry

1 Answers

1
votes

Solved :

The application was unable to write to db file created in /tmp by tempfile.mkstemp(). using a relative local path, it works.