tortoise-orm icon indicating copy to clipboard operation
tortoise-orm copied to clipboard

Support pytest verbs test-system (was: Unable to rollback transactions using pytest and in_transaction context manager)

Open nsidnev opened this issue 5 years ago • 7 comments

Describe the bug I wrote a pytest fixture that should be automatically run for each test. This fixture starts a new transaction using the in_transaction context manager, yields, and then tries to roll back. But the fixture breaks when it tries to roll back with an error that the token was created in a different context like this:

ValueError: <Token var=<ContextVar name='default' default=<tortoise.backends.asyncpg.client.AsyncpgDBClient object at 0x7f8c294a6670> at 0x7f8c294b2810> at 0x7f8c285a7140> was created in a different Context

To Reproduce

  1. Install pytest, pytest-asyncio, tortoise-orm and asyncpg.
  2. Start the PostgreSQL server and create a new table with the contents:
CREATE TABLE mymodelfortest (
	id VARCHAR(255) PRIMARY KEY,
	name TEXT DEFAULT 'text'
);
  1. Create a new test_tortoise.py test file:
import contextlib

import pytest
from tortoise import Tortoise, fields, models
from tortoise.transactions import in_transaction


class MyModelForTest(models.Model):
    id = fields.CharField(pk=True, max_length=255)
    name = fields.TextField(default="text")


@pytest.fixture(autouse=True)
async def init_db():
    await Tortoise.init(
        db_url="postgres://postgres:postgres@localhost:5432/postgres",
        modules={"models": ["test_tortoise"]},
    )
    yield
    await Tortoise.close_connections()


@pytest.fixture(autouse=True)
async def db_transaction(init_db):
    with contextlib.suppress(RuntimeError):
        async with in_transaction():
            yield
            raise RuntimeError


@pytest.mark.asyncio
async def test_a():
    await MyModelForTest.create(id="test")
    assert (await MyModelForTest.all()) == [MyModelForTest(id="test")]


@pytest.mark.asyncio
async def test_b():
    assert (await MyModelForTest.all()) == []

  1. Run pytest and get a message about tests teardown errors and some failed tests:
$ pytest test_tortoise.py
====================================================================================== test session starts ======================================================================================
platform linux -- Python 3.8.1, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/nik/CodeSources/tortoise-test
plugins: asyncio-0.10.0
collected 2 items                                                                                                                                                                               

test_tortoise.py .EFE                                                                                                                                                                     [100%]

============================================================================================ ERRORS =============================================================================================
__________________________________________________________________________________ ERROR at teardown of test_a __________________________________________________________________________________

init_db = None

    @pytest.fixture(autouse=True)
    async def db_transaction(init_db):
        with contextlib.suppress(RuntimeError):
            async with in_transaction():
                yield
>               raise RuntimeError
E               RuntimeError

test_tortoise.py:28: RuntimeError

During handling of the above exception, another exception occurred:

init_db = None

    @pytest.fixture(autouse=True)
    async def db_transaction(init_db):
        with contextlib.suppress(RuntimeError):
            async with in_transaction():
                yield
>               raise RuntimeError

test_tortoise.py:28: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <tortoise.backends.base.client.TransactionContextPooled object at 0x7f8c28e240a0>, exc_type = <class 'RuntimeError'>, exc_val = RuntimeError()
exc_tb = <traceback object at 0x7f8c285bc1c0>

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        if not self.connection._finalized:
            if exc_type:
                # Can't rollback a transaction that already failed.
                if exc_type is not TransactionManagementError:
                    await self.connection.rollback()
            else:
                await self.connection.commit()
>       current_transaction_map[self.connection_name].reset(self.token)
E       ValueError: <Token var=<ContextVar name='default' default=<tortoise.backends.asyncpg.client.AsyncpgDBClient object at 0x7f8c294a6670> at 0x7f8c294b2810> at 0x7f8c285a7140> was created in a different Context

../../.virtualenvs/tortoise-test-aUW8XuHC-py3.8/lib/python3.8/site-packages/tortoise/backends/base/client.py:173: ValueError
__________________________________________________________________________________ ERROR at teardown of test_b __________________________________________________________________________________

init_db = None

    @pytest.fixture(autouse=True)
    async def db_transaction(init_db):
        with contextlib.suppress(RuntimeError):
            async with in_transaction():
                yield
>               raise RuntimeError
E               RuntimeError

test_tortoise.py:28: RuntimeError

During handling of the above exception, another exception occurred:

init_db = None

    @pytest.fixture(autouse=True)
    async def db_transaction(init_db):
        with contextlib.suppress(RuntimeError):
            async with in_transaction():
                yield
>               raise RuntimeError

test_tortoise.py:28: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <tortoise.backends.base.client.TransactionContextPooled object at 0x7f8c284e7100>, exc_type = <class 'RuntimeError'>, exc_val = RuntimeError()
exc_tb = <traceback object at 0x7f8c29516c00>

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        if not self.connection._finalized:
            if exc_type:
                # Can't rollback a transaction that already failed.
                if exc_type is not TransactionManagementError:
                    await self.connection.rollback()
            else:
                await self.connection.commit()
>       current_transaction_map[self.connection_name].reset(self.token)
E       ValueError: <Token var=<ContextVar name='default' default=<tortoise.backends.asyncpg.client.AsyncpgDBClient object at 0x7f8c28e155b0> at 0x7f8c285d8e00> at 0x7f8c284db680> was created in a different Context

../../.virtualenvs/tortoise-test-aUW8XuHC-py3.8/lib/python3.8/site-packages/tortoise/backends/base/client.py:173: ValueError
=========================================================================================== FAILURES ============================================================================================
____________________________________________________________________________________________ test_b _____________________________________________________________________________________________

    @pytest.mark.asyncio
    async def test_b():
>       assert (await MyModelForTest.all()) == []
E       assert [<MyModelForTest: test>] == []
E         Left contains one more item: <MyModelForTest: test>
E         Use -v to get the full diff

test_tortoise.py:39: AssertionError
============================================================================ 1 failed, 1 passed, 2 errors in 20.18s =============================================================================

Expected behavior Transactions should be able to roll back after the test is completed and the fixture tries to teardown, and all tests must pass.

Additional context OS: Linux 5.5.2-arch1-1 PostgreSQL: 12.1-alpine for docker Docker version: 19.03.5-ce Python: 3.8.1 Dependencies:

aiosqlite==0.11.0
asyncpg==0.20.1
attrs==19.3.0
ciso8601==2.1.3
more-itertools==8.2.0
packaging==20.1
pluggy==0.13.1
py==1.8.1
pyparsing==2.4.6
PyPika==0.35.21
pytest==5.3.5
pytest-asyncio==0.10.0
six==1.14.0
tortoise-orm==0.15.9
typing-extensions==3.7.4.1
wcwidth==0.1.8

I previously used tortoise-orm 0.14.1 and solved this issue using this fixture:

@pytest.fixture(autouse=True)
async def db_transaction():
    transaction = await start_transaction()
    yield
    await transaction.rollback()

But since start_transaction is deprecated, so I tried changing it for the in_transaction context manager and ran into this problem.

nsidnev avatar Feb 07 '20 14:02 nsidnev

We depend on the UnitTest infrastructure to manage DB states during testing. The pytest verbs are a totally different system unfortunately.

You can use pytest, and write tests like so: https://tortoise-orm.readthedocs.io/en/latest/contrib/unittest.html

Or you could consider adding support for pytest verbs as a PR?

grigi avatar Feb 10 '20 06:02 grigi

Ok, I get that. Right now I fixed it similar to how it was done in the helpers from tortoise.contrib.test. I just handle database schema migrations(using alembic) in a separate fixture without transactions. But a little later I would like to try to add support for pytest fixtures, if someone else does not.

nsidnev avatar Feb 10 '20 14:02 nsidnev

I'm curious, how did you use alembic with Tortoise? Last time I looked it appears very tightly coupled to SQLAlchemy?

grigi avatar Feb 10 '20 18:02 grigi

I'm curious, how did you use alembic with Tortoise? Last time I looked it appears very tightly coupled to SQLAlchemy?

Well, I just do not use sqlalchemy functionality as ORM, and alembic in this way becomes just a tool for managing database migration in my projects. I write the migrations myself manually. In my opinion, alembic in combination with sqlalchemy has a good enough dsl for creating migrations, and as a tool it turned out to be more convenient for me than other tools for migrations.

nsidnev avatar Feb 11 '20 13:02 nsidnev

@grigi I faced the same issue. I'm using pytest with tortoise pretty intensely and my test uses the transactions. So, I saw tortoise.contrib.test helpers and found TruncationTestCase that should resolve the issue, as the doc-string says. But it produces the query that just delete all records from a table. Can you please advise how I can implement the same helper but with deleting only those records that was created by a test? There is another approach according to TODO.

madnesspie avatar May 08 '20 16:05 madnesspie

@nsidnev

I'm facing the same issue. And can't handle it for a couple of days.

Could you please provide your solution?

I fixed it similar to how it was done in the helpers from tortoise.contrib.test - what exactly helped you to make it work?

alexshurik avatar Jun 22 '22 16:06 alexshurik

sorry, but I haven't worked on a project that used this for almost 2 years and I don't have access to it, so I won't be able to provide a solution :(

nsidnev avatar Jun 22 '22 20:06 nsidnev