如何正确地 运行 连续测试查询 Flask-SQLAlchemy 数据库?

How to properly run consecutive tests querying a Flask-SQLAlchemy database?

我正在为使用 SQLAlchemy 作为 ORM 的 Flask 项目设置单元测试。对于我的测试,我需要在每次 运行 单个单元测试时设置一个新的测试数据库。不知何故,我似乎无法 运行 查询数据库的连续测试,即使我 运行 这些测试是孤立的,它们也会成功。

我使用 flask-testing 包,并遵循他们的文档 here

这是一个说明问题的工作示例:

app.py:

from flask import Flask


def create_app():
    app = Flask(__name__)
    return app


if __name__ == '__main__':
    app = create_app()
    app.run(port=8080)

database.py:

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

models.py:

from database import db


class TestModel(db.Model):
    """Model for testing."""

    __tablename__ = 'test_models'
    id = db.Column(db.Integer,
                   primary_key=True
                   )

test/__init__.py:

from flask_testing import TestCase

from app import create_app
from database import db


class BaseTestCase(TestCase):
    def create_app(self):
        app = create_app()
        app.config.update({
            'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
            'SQLALCHEMY_TRACK_MODIFICATIONS': False,
            'TESTING': True
        })
        db.init_app(app)
        return app

    def setUp(self):
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

test/test_app.py:

from models import TestModel
from test import BaseTestCase
from database import db


test_model = TestModel()


class TestApp(BaseTestCase):
    """WebpageEnricherController integration test stubs"""

    def _add_to_db(self, record):
        db.session.add(record)
        db.session.commit()
        self.assertTrue(record in db.session)

    def test_first(self):
        """
        This test runs perfectly fine
        """
        self._add_to_db(test_model)
        result = db.session.query(TestModel).first()
        self.assertIsNotNone(result, 'Nothing in the database')

    def test_second(self):
        """
        This test runs fine in isolation, but fails if run consecutively
        after the first test
        """
        self._add_to_db(test_model)
        result = db.session.query(TestModel).first()
        self.assertIsNotNone(result, 'Nothing in the database')


if __name__ == '__main__':
    import unittest
    unittest.main()

所以,如果 运行 孤立,我可以 运行 TestApp.test_firstTestApp.test_second 很好。如果我连续 运行 它们,第一个测试通过,但第二个测试失败:

=================================== FAILURES ===================================
_____________________________ TestApp.test_second ______________________________

self = <test.test_app.TestApp testMethod=test_second>

    def test_second(self):
        """
        This test runs fine in isolation, but fails if run consecutively
        after the first test
        """
        self._add_to_db(test_model)
        result = db.session.query(TestModel).first()
>       self.assertIsNotNone(result, 'Nothing in the database')
E       AssertionError: unexpectedly None : Nothing in the database

数据库设置和拆卸过程中出了点问题,但我不知道是什么原因。我该如何正确设置?

答案是,您通过重复使用在模块范围 (test_model = TestModel()) 中定义一次的单个 TestModel 实例,在一个测试和下一个测试之间泄漏状态。

该实例在第一个测试开始时的状态是 transient:

an instance that’s not in a session, and is not saved to the database; i.e. it has no database identity. The only relationship such an object has to the ORM is that its class has a mapper() associated with it.

第二次测试开始时对象的状态是detached:

Detached - an instance which corresponds, or previously corresponded, to a record in the database, but is not currently in any session. The detached object will contain a database identity marker, however because it is not associated with a session, it is unknown whether or not this database identity actually exists in a target database. Detached objects are safe to use normally, except that they have no ability to load unloaded attributes or attributes that were previously marked as “expired”.

测试之间的这种相互依赖几乎总是一个坏主意。您可以在每次测试结束时对对象使用 make_transient()

class BaseTestCase(TestCase):
    ...
    def tearDown(self):
        db.session.remove()
        db.drop_all()
        make_transient(test_model)

或者您应该为每个测试构造一个新的 TestModel 实例:

class BaseTestCase(TestCase):
    ...
    def setUp(self):
        db.create_all()
        self.test_model = TestModel()


class TestApp(BaseTestCase):
    ...
    def test_xxxxx(self):
        self._add_to_db(self.test_model)

我认为后者是更好的选择,因为在测试之间没有任何其他泄漏状态的危险。