pytest-capsqlalchemy¶
Pytest plugin to allow capturing SQLAlchemy queries.
Getting Started¶
Installation¶
Install pytest-capsqlalchemy via pip or your preferred package manager:
Configuration¶
In order to use the fixtures provided by the plugin you also need to define a db_engine fixture
in your conftest.py. This fixture should return an AsyncEngine instance. For example:
import pytest
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
@pytest.fixture(scope="session")
def db_engine() -> AsyncEngine:
return create_async_engine("postgresql+asyncpg://user:pass@localhost:5432/db")
Usage¶
Counting number of queries¶
Here is a basic example with asserts on the number of queries being performed
async def test_query_count(db_session, capsqlalchemy):
await db_session.execute(text("SELECT 1"))
capsqlalchemy.assert_query_count(1, include_tcl=False) # (1)!
capsqlalchemy.assert_query_count(3, include_tcl=True) # (2)!
- The
capsqlfixturestarts recording the queries which are sent to the database at the beginning of the test. As TCL statements (BEGIN,COMMIT,ROLLBACK) are not included in this assert, onlySELECT 1is counted - This time TCL statements are being counted and since by default SQLAlchemy is configured in
autobeginmode, there are 3 queries which are being counted:BEGIN,SELECT 1andROLLBACK
Capturing queries in specific context¶
You can also use capqsqlalchemy as a context manager to check only for the queries being performed in a specific
section of your code. For example:
async def test_context(db_session, capsqlalchemy):
capsqlalchemy.assert_query_count(0, include_tcl=False) # (1)!
await db_session.execute(text("SELECT 1"))
capsqlalchemy.assert_query_count(1, include_tcl=False) # (2)!
with capsqlalchemy:
await db_session.execute(text("SELECT 2"))
await db_session.execute(text("SELECT 3"))
capsqlalchemy.assert_query_count(2, include_tcl=False) # (3)!
capsqlalchemy.assert_query_count(3, include_tcl=False) # (4)!
- No queries have been executed yet
- Just like in the previous example only
SELECT 1has been captured yet - Since the assert is being performed inside the
withblock onlySELECT 2andSELECT 3will be counted - When using a context manager, the outer scope still counts the inner scope queries, so this time all
SELECT 1,SELECT 2andSELECT 3are captured
Checking exact queries¶
You can also write tests that the exact query you expected was generated and exectured by SQLALchemy. Here's an example with SQLAlchemy 2.0 ORM:
async def test_exact_query(db_session, capsqlalchemy):
await db_session.execute(select(Order.recipient).where(Order.id == 123))
capsqlalchemy.assert_captured_queries( # (1)!
"SELECT orders.id, orders.recipient \nFROM orders \nWHERE orders.id = :id_1",
include_tcl=False,
)
capsqlalchemy.assert_captured_queries( # (2)!
"SELECT orders.id, orders.recipient \nFROM orders \nWHERE orders.id = 123",
bind_params=True,
include_tcl=False,
)
- By default the of the queries will not be binded, so
123is replaced with:id_1 - If we want to check that the query is exactly what we expected we can pass
bind_params=Trueand we'll get the full query
Checking the query types¶
There are also cases where we care about the queries being performed beyond just their count but we don't care about their structure in such a detail -- that's where we can check that the captured query types are are what we expect:
from pytest_capsqlalchemy import SQLExpressionType
async def test_query_types(db_session, capsqlalchemy):
await db_session.execute(select(Order))
await db_session.commit()
async with db_session.begin():
await db_session.execute(select(Order).where(Order.id == 123))
await db_session.execute(select(text("1")))
db_session.add(Order(recipient="John Doe"))
capsqlalchemy.assert_query_types( # (1)!
SQLExpressionType.BEGIN,
SQLExpressionType.SELECT,
SQLExpressionType.COMMIT,
SQLExpressionType.BEGIN,
SQLExpressionType.SELECT,
SQLExpressionType.SELECT,
SQLExpressionType.INSERT,
SQLExpressionType.COMMIT,
)
capsqlalchemy.assert_query_types( # (2)!
"SELECT", # (3)!
"SELECT",
"SELECT",
"INSERT",
include_tcl=False,
)
assert_query_typesallows us to check that the captured queries are aBEGIN, aSELECT, aCOMMITetc. without specifying their exact structure- Similarly to the other assertion methods, we can exclude the TCL statements if we don't care about them
- We can also use strings instead of enum values to make the assert shorter to write
Warning
The order of the arguments matters -- it's the same order that the queries have been captured in.
For example, with the following setup:
this will pass:
but this will fail: