Как правильно запускать последовательные тесты, запрашивающие базу данных Flask-SQLAlchemy?

Я настраиваю модульное тестирование для проекта Flask, используя SQLAlchemy в качестве ORM. Для моих тестов мне нужно настраивать новую тестовую базу данных каждый раз, когда я запускаю один модульный тест. Почему-то я не могу запускать последовательные тесты, запрашивающие базу данных, даже если я запускаю эти тесты изолированно, они завершаются успешно.

Я использую пакет flask-testing и следую их документации здесь.

Вот рабочий пример, иллюстрирующий проблему:

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_first и TestApp.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

Что-то идет не так в настройке и демонтаже базы данных, но я не могу понять, что именно. Как правильно это настроить?


person Sven van der Burg    schedule 06.05.2019    source источник


Ответы (1)


Ответ заключается в том, что вы пропускаете состояние между одним тестом и другим, повторно используя один экземпляр TestModel, определенный один раз в области модуля (test_model = TestModel()).

Состояние этого экземпляра в начале первого теста — transient:

экземпляр, который не находится в сеансе и не сохраняется в базе данных; то есть он не имеет идентификатора базы данных. Единственное отношение, которое такой объект имеет к ORM, состоит в том, что его класс имеет связанный с ним mapper().

Состояние объекта в начале второго теста detached:

Отсоединенный - экземпляр, который соответствует или ранее соответствовал записи в базе данных, но в настоящее время не находится ни в одном сеансе. Отсоединенный объект будет содержать маркер удостоверения базы данных, однако, поскольку он не связан с сеансом, неизвестно, действительно ли этот идентификатор базы данных существует в целевой базе данных. Отсоединенные объекты безопасны для обычного использования, за исключением того, что у них нет возможности загружать незагруженные атрибуты или атрибуты, которые ранее были помечены как «истекшие».

Такая взаимозависимость между тестами почти всегда является плохой идеей. Вы можете использовать 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)

Я думаю, что последнее является лучшим выбором, поскольку нет опасности переноса какого-либо другого состояния с утечкой между тестами.

person SuperShoot    schedule 07.05.2019