Skillversitas

Integration Testing

The integration test runs the code against the real dependency: the database, the HTTP endpoint, the filesystem - the mock the unit's, the session fixture the shared resource (the DB created once, the dropped after), and the pytestmark = pytest.mark.integration the select so the unit run (-m "not integration") stays fast. The testcontainers is the container's DB (the no local install, the versioned), and the ordering (the -p no:randomly) is the state's guard. This chapter covers the session DB fixture (the create, the migrate, the drop), the integration mark (the -m select), the testcontainers (the container's DB), and the ordering the state depends on.

The Session DB Fixture

# the SESSION DB (the create ONCE, the drop after
#   the last test, the migrate in between)
import pytest
import texttools.db as db

@pytest.fixture(scope="session")
def database():
    conn = db.create_test_db()   # the create
    db.migrate(conn)             # the schema
    yield conn
    db.drop_test_db(conn)        # the drop

@pytest.fixture
def sample_row(database):
    # the FUNCTION (the fresh per test, the rollback
    #   after - the state's the guard)
    row = db.insert(database, "word", "fox")
    yield row
    db.rollback(database)

def test_read(sample_row):
    assert db.get(database, sample_row) == "fox"

def test_update(sample_row, database):
    db.update(database, sample_row, "fox2")
    assert db.get(database, sample_row) == "fox2"

The integration test's setup is the two-scope split: the database is the session (the create once, the migrate, the drop after the last test - the expensive, the shared) and the sample_row is the function (the insert per test, the rollback after - the fresh, the isolated). The rule is the one the unit follows at the real scale: the expensive the session, the per-test the function, the shared read-only or the transaction-rolled-back. The database fixture is the dependency the sample_row requests (the graph, the depth-first), and the integration mark (the next section) is the select that keeps the unit run fast - the pytest -m "not integration" the dev's loop, the pytest -m integration the CI's dedicated job.

The integration Mark and the testcontainers

# the MARK (the the select, the the unit's the fast):
#   the tests/integration/conftest.py
import pytest

pytestmark = pytest.mark.integration   # the whole
                                       #   module the
                                       #   integration

# the SELECT (the two lanes):
pytest -m "not integration"   # the UNIT (the fast)
pytest -m "integration"       # the INTEGRATION (the
                              #   the CI's the job)

# the TESTCONTAINERS (the the CONTAINER's DB, the the
#   the no-local, the the versioned):
from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope="session")
def db_url():
    with PostgresContainer("postgres:16") as c:
        yield c.get_connection_url()

The integration mark is the select: the pytestmark = pytest.mark.integration at the module level applies it to every test in the file (the tests/integration/ directory, the whole module the integration), and the pytest -m "not integration" is the unit run (the fast, the dev's loop) versus the pytest -m integration the dedicated job (the CI's, the nightly). The testcontainers is the container's dependency: the PostgresContainer("postgres:16") starts the image (the versioned, the no local install, the get_connection_url the DSN), the session scope shares it across the tests, and the with stops it after - the integration test's DB without the machine's. The ordering is the state's guard: the pytest-randomly (the shuffle) breaks the order-dependent test, so the -p no:randomly (or the randomly disable in the ini) is the integration run's rule, and the next chapter's CI wires the two lanes (the unit, the integration) into the pipeline.

The Working Rules

  • The expensive the session, the per-test the function. The database (the create once, the migrate, the drop) the session, the sample_row (the insert, the rollback) the function - the unit's rule at the real scale, the shared read-only or the rolled-back.
  • The integration mark is the lane, the -m the select. The pytestmark the module's apply, the -m "not integration" the fast unit loop, the -m integration the CI's dedicated job - the two lanes, the no slow unit run.
  • The testcontainers is the container's DB, the versioned. The PostgresContainer("postgres:16") the image, the get_connection_url the DSN, the session share, the with the stop - the no local install, the machine's no state.
  • The ordering is the state's guard, the -p no:randomly the rule. The shuffle breaks the order-dependent, the integration run disables it (the ini or the flag), and the fixture's rollback is the per-test isolation the order cannot replace.

Summary

The integration test runs the code against the real dependency (the database, the HTTP, the filesystem), and the setup is the two-scope split: the database fixture the session (the create once, the migrate, the drop after the last) and the sample_row the function (the insert per test, the rollback after - the fresh, the isolated). The integration mark (the pytestmark the module's apply) is the select: the -m "not integration" the fast unit loop, the -m integration the CI's dedicated job. The testcontainers (the PostgresContainer("postgres:16") the image, the get_connection_url the DSN, the session share) is the container's dependency without the machine's, and the -p no:randomly is the ordering guard the state-dependent test needs. The next chapter's CI wires the lanes into the pipeline - the pytest the job, the --junitxml the report, the -n the parallel, the matrix the version.