Как правильно запустить последовательные тесты, запрашивающие базу данных Flask-SQLAlchemy? - PullRequest
1 голос
/ 06 мая 2019

Я настраиваю модульное тестирование для проекта 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

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

1 Ответ

1 голос
/ 07 мая 2019

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

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

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

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

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)

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

...