question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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

See original GitHub issue

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.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:5
  • Comments:7 (5 by maintainers)

github_iconTop GitHub Comments

3reactions
nsidnevcommented, Feb 10, 2020

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.

1reaction
grigicommented, Feb 10, 2020

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?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Rollback transactions not working with py.test and Flask
It looks like you are trying to join a Session into an External Transaction and you are using flask-sqlalchemy. Your code is not...
Read more >
Python PostgreSQL - Transaction management using Commit ...
Commit() and rollback() are two methods of the connection class that may be used to stop a transaction. The commit() function is used...
Read more >
Transactions and Connection Management
from sqlalchemy.orm import ; with session. ; session = Session ; # expunges all objects, releases all transactions unconditionally # (with rollback) ...
Read more >
Transactions - Snowflake Documentation
This is true regardless of what ended the preceding transaction (e.g. a DDL statement, or an explicit commit or rollback). The first DML...
Read more >
testing that a session is committed correctly - Google Groups
@pytest.fixture(scope='session') def db(client): engine ... of whether the "with session.transaction" is there, and with it there, it always fails.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found