[{"data":1,"prerenderedAt":1512},["ShallowReactive",2],{"page-\u002Fintegration-database-and-service-testing\u002F":3},{"id":4,"title":5,"body":6,"description":1490,"extension":1491,"meta":1492,"navigation":249,"path":1508,"seo":1509,"stem":1510,"__hash__":1511},"content\u002Fintegration-database-and-service-testing\u002Findex.md","Integration, Database & Service Testing",{"type":7,"value":8,"toc":1477},"minimark",[9,13,37,42,45,172,180,184,187,194,200,215,362,384,391,395,398,476,504,643,647,650,653,738,758,762,769,781,785,788,794,800,806,899,906,913,917,920,926,963,1000,1014,1024,1074,1084,1094,1216,1220,1223,1230,1296,1311,1318,1322,1386,1390,1396,1406,1412,1418,1424,1428,1466,1473],[10,11,12],"p",{},"Integration tests are where test suites go to become slow and unreliable, and the reasons are nearly always structural rather than intrinsic. A suite that starts a Postgres container per test, deletes rows in teardown, seeds data by calling the application's own API, and asserts against whatever the staging environment happens to be running today will be slow, order-dependent, and red for reasons unrelated to the change under review. The same coverage, structured differently — one container per session, one transaction per test, data built by explicit factories, contracts verified against a schema — runs in a fraction of the time and fails only when something is genuinely broken.",[10,14,15,16,21,22,26,27,31,32,36],{},"This section covers that restructuring: ",[17,18,20],"a",{"href":19},"\u002Fintegration-database-and-service-testing\u002Fdatabase-fixtures-and-transactional-tests\u002F","transactional database fixtures"," that give per-test isolation without per-test setup, ",[17,23,25],{"href":24},"\u002Fintegration-database-and-service-testing\u002Fspinning-up-services-with-testcontainers\u002F","Testcontainers lifecycles"," that start real services once and wait for them properly, ",[17,28,30],{"href":29},"\u002Fintegration-database-and-service-testing\u002Ftest-data-factories-and-builders\u002F","data factories"," that make a test's preconditions readable, and ",[17,33,35],{"href":34},"\u002Fintegration-database-and-service-testing\u002Fcontract-testing-for-http-apis\u002F","contract testing for HTTP APIs"," that moves the breakage detection to the side that caused it. Readers are assumed comfortable with pytest fixtures, Docker, and at least one SQL toolkit.",[38,39,41],"h2",{"id":40},"the-layers-and-what-each-one-is-allowed-to-be-slow-about","The layers, and what each one is allowed to be slow about",[10,43,44],{},"The useful mental model is not a pyramid but a set of concentric boundaries. Each layer is allowed to cross exactly one more boundary than the layer inside it, and the cost of a test is dominated by the outermost boundary it crosses.",[46,47,50,168],"figure",{"className":48},[49],"diagram",[51,52,59,60,59,64,59,68,59,76,59,86,59,96,59,101,59,106,59,114,59,120,59,124,59,128,59,132,59,137,59,141,59,145,59,150,59,155,59,159,59,163],"svg",{"viewBox":53,"role":54,"ariaLabelledBy":55,"xmlns":58},"0 0 820 320","img",[56,57],"layers-t","layers-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[61,62,63],"title",{"id":56},"Test layers by the boundary each one crosses",[65,66,67],"desc",{"id":57},"Four nested bands. The innermost runs pure business logic in microseconds. The next adds an in-memory fake repository in under a millisecond. The third crosses a real database connection in tens of milliseconds. The outermost crosses the network to a containerised service in hundreds of milliseconds, and each band lists what it is uniquely able to catch.",[69,70],"rect",{"x":71,"y":71,"width":72,"height":73,"rx":74,"fill":75},"0","820","320","14","#fffdf8",[77,78,85],"text",{"x":79,"y":80,"textAnchor":81,"fontSize":82,"fontWeight":83,"fill":84},"410","28","middle","16","700","#3d405b","Each layer buys one more boundary, and pays for it",[69,87],{"x":88,"y":89,"width":90,"height":91,"rx":92,"fill":93,"stroke":94,"strokeWidth":95},"30","48","760","58","10","#f4f1de","rgba(61,64,91,0.35)","1.5",[77,97,100],{"x":89,"y":98,"fontSize":99,"fontWeight":83,"fill":84},"72","12.5","no boundary — pure functions",[77,102,105],{"x":89,"y":103,"fontSize":104,"fill":84},"92","11","catches: arithmetic, branching, validation rules",[77,107,113],{"x":108,"y":109,"textAnchor":110,"fontSize":111,"fontWeight":83,"fill":112},"762","82","end","12","#2a5f49","microseconds",[69,115],{"x":88,"y":116,"width":90,"height":91,"rx":92,"fill":117,"stroke":118,"strokeWidth":119},"114","#e6f0ea","#81b29a","1.8",[77,121,123],{"x":89,"y":122,"fontSize":99,"fontWeight":83,"fill":84},"138","object boundary — in-memory fakes",[77,125,127],{"x":89,"y":126,"fontSize":104,"fill":84},"158","catches: orchestration, error handling, retry policy",[77,129,131],{"x":108,"y":130,"textAnchor":110,"fontSize":111,"fontWeight":83,"fill":112},"148","\u003C 1 ms",[69,133],{"x":88,"y":134,"width":90,"height":91,"rx":92,"fill":135,"stroke":136,"strokeWidth":119},"180","#f7f0da","#f2cc8f",[77,138,140],{"x":89,"y":139,"fontSize":99,"fontWeight":83,"fill":84},"204","process boundary — real database, one transaction",[77,142,144],{"x":89,"y":143,"fontSize":104,"fill":84},"224","catches: SQL, constraints, isolation, migrations",[77,146,149],{"x":108,"y":147,"textAnchor":110,"fontSize":111,"fontWeight":83,"fill":148},"214","#8a5a00","10–50 ms",[69,151],{"x":88,"y":152,"width":90,"height":91,"rx":92,"fill":153,"stroke":154,"strokeWidth":119},"246","#fbe9e3","#e07a5f",[77,156,158],{"x":89,"y":157,"fontSize":99,"fontWeight":83,"fill":84},"270","network boundary — containerised service",[77,160,162],{"x":89,"y":161,"fontSize":104,"fill":84},"290","catches: wire formats, auth, timeouts, redelivery",[77,164,167],{"x":108,"y":165,"textAnchor":110,"fontSize":111,"fontWeight":83,"fill":166},"280","#8f3d22","100 ms – 2 s",[169,170,171],"figcaption",{},"A test that crosses the network boundary to assert a validation rule pays two hundred times over for coverage the innermost layer already had. The layering question is always \"what is the outermost boundary this assertion actually needs?\"",[10,173,174,175,179],{},"The corollary is a triage rule for an existing slow suite: find the tests crossing the outer boundaries and ask what each one uniquely catches. A test that spins up Postgres to check that an empty basket totals zero is a unit test wearing a container. Moving it inward is usually a one-line change to accept a repository argument, which is the argument made at length in ",[17,176,178],{"href":177},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002F","dependency injection for testability",".",[38,181,183],{"id":182},"transactional-isolation-beats-cleanup","Transactional isolation beats cleanup",[10,185,186],{},"The most consequential decision in a database suite is how a test leaves the database clean. There are three options and they are not equivalent.",[10,188,189,193],{},[190,191,192],"strong",{},"Delete in teardown"," is the intuitive one and the worst. It is O(tables) per test, it has to respect foreign-key order, it misses anything a test created through a path you forgot about, and an exception mid-test skips the cleanup entirely, poisoning every test after it.",[10,195,196,199],{},[190,197,198],{},"Recreate the schema per test"," is correct and roughly a hundred times too slow — hundreds of milliseconds of DDL for a test that does two inserts.",[10,201,202,205,206,210,211,214],{},[190,203,204],{},"Wrap each test in a transaction and roll it back"," is correct, cheap, and immune to mid-test failures, because the rollback happens in the fixture's ",[207,208,209],"code",{},"finally"," whether the test passed, failed or raised. The subtlety is that application code often calls ",[207,212,213],{},"commit()"," itself, which would end your outer transaction — the answer is a savepoint, and both SQLAlchemy and Django provide the machinery.",[216,217,222],"pre",{"className":218,"code":219,"language":220,"meta":221,"style":221},"language-python shiki shiki-themes github-light github-dark","import pytest\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import Session\n\n\n@pytest.fixture(scope=\"session\")\ndef engine():\n    # One engine, one pool, for the whole run. The DSN points at a container\n    # started once per session, not at a developer's local install.\n    return create_engine(\"postgresql+psycopg:\u002F\u002Ftest:test@localhost:5432\u002Ftest\")\n\n\n@pytest.fixture\ndef db_session(engine):\n    connection = engine.connect()\n    outer = connection.begin()                 # the transaction we will roll back\n    session = Session(bind=connection, join_transaction_mode=\"create_savepoint\")\n    try:\n        yield session                          # application code may commit freely\n    finally:\n        session.close()\n        outer.rollback()                       # undoes everything, always\n        connection.close()\n","python","",[207,223,224,232,238,244,251,256,262,268,274,280,286,291,296,302,308,314,320,326,332,338,344,350,356],{"__ignoreMap":221},[225,226,229],"span",{"class":227,"line":228},"line",1,[225,230,231],{},"import pytest\n",[225,233,235],{"class":227,"line":234},2,[225,236,237],{},"from sqlalchemy import create_engine, event\n",[225,239,241],{"class":227,"line":240},3,[225,242,243],{},"from sqlalchemy.orm import Session\n",[225,245,247],{"class":227,"line":246},4,[225,248,250],{"emptyLinePlaceholder":249},true,"\n",[225,252,254],{"class":227,"line":253},5,[225,255,250],{"emptyLinePlaceholder":249},[225,257,259],{"class":227,"line":258},6,[225,260,261],{},"@pytest.fixture(scope=\"session\")\n",[225,263,265],{"class":227,"line":264},7,[225,266,267],{},"def engine():\n",[225,269,271],{"class":227,"line":270},8,[225,272,273],{},"    # One engine, one pool, for the whole run. The DSN points at a container\n",[225,275,277],{"class":227,"line":276},9,[225,278,279],{},"    # started once per session, not at a developer's local install.\n",[225,281,283],{"class":227,"line":282},10,[225,284,285],{},"    return create_engine(\"postgresql+psycopg:\u002F\u002Ftest:test@localhost:5432\u002Ftest\")\n",[225,287,289],{"class":227,"line":288},11,[225,290,250],{"emptyLinePlaceholder":249},[225,292,294],{"class":227,"line":293},12,[225,295,250],{"emptyLinePlaceholder":249},[225,297,299],{"class":227,"line":298},13,[225,300,301],{},"@pytest.fixture\n",[225,303,305],{"class":227,"line":304},14,[225,306,307],{},"def db_session(engine):\n",[225,309,311],{"class":227,"line":310},15,[225,312,313],{},"    connection = engine.connect()\n",[225,315,317],{"class":227,"line":316},16,[225,318,319],{},"    outer = connection.begin()                 # the transaction we will roll back\n",[225,321,323],{"class":227,"line":322},17,[225,324,325],{},"    session = Session(bind=connection, join_transaction_mode=\"create_savepoint\")\n",[225,327,329],{"class":227,"line":328},18,[225,330,331],{},"    try:\n",[225,333,335],{"class":227,"line":334},19,[225,336,337],{},"        yield session                          # application code may commit freely\n",[225,339,341],{"class":227,"line":340},20,[225,342,343],{},"    finally:\n",[225,345,347],{"class":227,"line":346},21,[225,348,349],{},"        session.close()\n",[225,351,353],{"class":227,"line":352},22,[225,354,355],{},"        outer.rollback()                       # undoes everything, always\n",[225,357,359],{"class":227,"line":358},23,[225,360,361],{},"        connection.close()\n",[10,363,364,367,368,371,372,375,376,379,380,383],{},[207,365,366],{},"join_transaction_mode=\"create_savepoint\""," (SQLAlchemy 2.0) is the important argument: it tells the session to open a ",[207,369,370],{},"SAVEPOINT"," inside the connection's existing transaction, so a ",[207,373,374],{},"session.commit()"," inside the application releases the savepoint rather than committing the outer transaction. On 1.4 the same effect needed a manual ",[207,377,378],{},"after_transaction_end"," event listener that restarted the nested transaction — code that appears in a lot of older ",[207,381,382],{},"conftest.py"," files and can now be deleted.",[10,385,386,387,179],{},"What this buys in CI is measurable: a suite of 400 database tests goes from roughly four minutes of setup-dominated runtime to under thirty seconds, and the tests stop caring what order they run in. The detailed walkthrough, including the cases where savepoints are not enough, is in ",[17,388,390],{"href":389},"\u002Fintegration-database-and-service-testing\u002Fdatabase-fixtures-and-transactional-tests\u002Frolling-back-every-test-with-nested-transactions\u002F","rolling back every test with nested transactions",[38,392,394],{"id":393},"real-services-started-once-waited-for-properly","Real services, started once, waited for properly",[10,396,397],{},"Testcontainers starts a real Docker container from the test process and hands back a connection URL. The value is that the service is the actual one — the same Postgres version, the same Redis eviction behaviour, the same Kafka protocol — rather than an approximation whose differences surface in production.",[216,399,401],{"className":218,"code":400,"language":220,"meta":221,"style":221},"import pytest\nfrom testcontainers.postgres import PostgresContainer\n\n\n@pytest.fixture(scope=\"session\")\ndef postgres():\n    # Pinned tag: an unpinned \"latest\" makes the suite's behaviour depend on\n    # when the CI cache was last warmed.\n    with PostgresContainer(\"postgres:16-alpine\") as container:\n        yield container\n\n\n@pytest.fixture(scope=\"session\")\ndef dsn(postgres):\n    # The container maps 5432 to an ephemeral host port; never hardcode it.\n    return postgres.get_connection_url()\n",[207,402,403,407,412,416,420,424,429,434,439,444,449,453,457,461,466,471],{"__ignoreMap":221},[225,404,405],{"class":227,"line":228},[225,406,231],{},[225,408,409],{"class":227,"line":234},[225,410,411],{},"from testcontainers.postgres import PostgresContainer\n",[225,413,414],{"class":227,"line":240},[225,415,250],{"emptyLinePlaceholder":249},[225,417,418],{"class":227,"line":246},[225,419,250],{"emptyLinePlaceholder":249},[225,421,422],{"class":227,"line":253},[225,423,261],{},[225,425,426],{"class":227,"line":258},[225,427,428],{},"def postgres():\n",[225,430,431],{"class":227,"line":264},[225,432,433],{},"    # Pinned tag: an unpinned \"latest\" makes the suite's behaviour depend on\n",[225,435,436],{"class":227,"line":270},[225,437,438],{},"    # when the CI cache was last warmed.\n",[225,440,441],{"class":227,"line":276},[225,442,443],{},"    with PostgresContainer(\"postgres:16-alpine\") as container:\n",[225,445,446],{"class":227,"line":282},[225,447,448],{},"        yield container\n",[225,450,451],{"class":227,"line":288},[225,452,250],{"emptyLinePlaceholder":249},[225,454,455],{"class":227,"line":293},[225,456,250],{"emptyLinePlaceholder":249},[225,458,459],{"class":227,"line":298},[225,460,261],{},[225,462,463],{"class":227,"line":304},[225,464,465],{},"def dsn(postgres):\n",[225,467,468],{"class":227,"line":310},[225,469,470],{},"    # The container maps 5432 to an ephemeral host port; never hardcode it.\n",[225,472,473],{"class":227,"line":316},[225,474,475],{},"    return postgres.get_connection_url()\n",[10,477,478,479,482,483,487,488,491,492,495,496,499,500,179],{},"Two mistakes account for most Testcontainers pain. The first is scope: a ",[207,480,481],{},"function","-scoped container adds two to five seconds ",[484,485,486],"em",{},"per test",", which is how a fifty-test suite becomes a four-minute one. The second is readiness — a container that is ",[484,489,490],{},"running"," is not a container that is ",[484,493,494],{},"accepting connections",", and the gap is where ",[207,497,498],{},"time.sleep(5)"," gets added and then doubled every time CI is busy. Every Testcontainers module ships a wait strategy that polls the real readiness signal (a log line, a port, a health query); using it removes both the sleep and the flake, as shown in ",[17,501,503],{"href":502},"\u002Fintegration-database-and-service-testing\u002Fspinning-up-services-with-testcontainers\u002Fwaiting-for-container-readiness-without-sleep\u002F","waiting for container readiness without sleep",[46,505,507,640],{"className":506},[49],[51,508,59,513,59,516,59,519,59,523,59,527,59,531,59,537,59,543,59,546,59,549,59,553,59,557,59,560,59,563,59,566,59,569,59,572,59,575,59,580,59,584,59,587,59,590,59,593,59,596,59,599,59,602,59,605,59,608,59,611,59,614,59,617,59,620,59,623,59,626,59,630,59,635],{"viewBox":509,"role":54,"ariaLabelledBy":510,"xmlns":58},"0 0 840 250",[511,512],"ctr-t","ctr-d",[61,514,515],{"id":511},"Container lifecycle against test execution",[65,517,518],{"id":512},"Two timelines. In the per-test lifecycle, each test pays start-up and shutdown around a short body, so the run is dominated by container time. In the session lifecycle, a single start-up precedes all tests, each of which runs a short transaction, and a single shutdown follows.",[69,520],{"x":71,"y":71,"width":521,"height":522,"rx":74,"fill":75},"840","250",[77,524,526],{"x":525,"y":80,"textAnchor":81,"fontSize":82,"fontWeight":83,"fill":84},"420","Where the wall clock goes",[77,528,530],{"x":529,"y":98,"fontSize":111,"fontWeight":83,"fill":166},"26","per-test container",[69,532],{"x":134,"y":533,"width":534,"height":88,"rx":535,"fill":153,"stroke":154,"strokeWidth":536},"54","86","7","1.6",[77,538,542],{"x":539,"y":540,"textAnchor":81,"fontSize":541,"fill":84},"223","74","10.5","start 3 s",[69,544],{"x":157,"y":533,"width":545,"height":88,"rx":535,"fill":117,"stroke":118,"strokeWidth":536},"40",[77,547,548],{"x":161,"y":540,"textAnchor":81,"fontSize":541,"fill":84},"test",[69,550],{"x":551,"y":533,"width":552,"height":88,"rx":535,"fill":153,"stroke":154,"strokeWidth":536},"314","70",[77,554,556],{"x":555,"y":540,"textAnchor":81,"fontSize":541,"fill":84},"349","stop 1 s",[69,558],{"x":559,"y":533,"width":534,"height":88,"rx":535,"fill":153,"stroke":154,"strokeWidth":536},"392",[77,561,542],{"x":562,"y":540,"textAnchor":81,"fontSize":541,"fill":84},"435",[69,564],{"x":565,"y":533,"width":545,"height":88,"rx":535,"fill":117,"stroke":118,"strokeWidth":536},"482",[77,567,548],{"x":568,"y":540,"textAnchor":81,"fontSize":541,"fill":84},"502",[69,570],{"x":571,"y":533,"width":552,"height":88,"rx":535,"fill":153,"stroke":154,"strokeWidth":536},"526",[77,573,556],{"x":574,"y":540,"textAnchor":81,"fontSize":541,"fill":84},"561",[77,576,579],{"x":577,"y":540,"fontSize":578,"fontWeight":83,"fill":166},"640","11.5","…and so on",[77,581,583],{"x":529,"y":582,"fontSize":111,"fontWeight":83,"fill":112},"150","session container",[69,585],{"x":134,"y":586,"width":534,"height":88,"rx":535,"fill":135,"stroke":136,"strokeWidth":536},"132",[77,588,542],{"x":539,"y":589,"textAnchor":81,"fontSize":541,"fill":84},"152",[69,591],{"x":592,"y":586,"width":545,"height":88,"rx":535,"fill":117,"stroke":118,"strokeWidth":536},"272",[77,594,548],{"x":595,"y":589,"textAnchor":81,"fontSize":541,"fill":84},"292",[69,597],{"x":598,"y":586,"width":545,"height":88,"rx":535,"fill":117,"stroke":118,"strokeWidth":536},"316",[77,600,548],{"x":601,"y":589,"textAnchor":81,"fontSize":541,"fill":84},"336",[69,603],{"x":604,"y":586,"width":545,"height":88,"rx":535,"fill":117,"stroke":118,"strokeWidth":536},"360",[77,606,548],{"x":607,"y":589,"textAnchor":81,"fontSize":541,"fill":84},"380",[69,609],{"x":610,"y":586,"width":545,"height":88,"rx":535,"fill":117,"stroke":118,"strokeWidth":536},"404",[77,612,548],{"x":613,"y":589,"textAnchor":81,"fontSize":541,"fill":84},"424",[69,615],{"x":616,"y":586,"width":545,"height":88,"rx":535,"fill":117,"stroke":118,"strokeWidth":536},"448",[77,618,548],{"x":619,"y":589,"textAnchor":81,"fontSize":541,"fill":84},"468",[69,621],{"x":622,"y":586,"width":552,"height":88,"rx":535,"fill":135,"stroke":136,"strokeWidth":536},"492",[77,624,556],{"x":625,"y":589,"textAnchor":81,"fontSize":541,"fill":84},"527",[77,627,629],{"x":628,"y":589,"fontSize":578,"fontWeight":83,"fill":112},"580","one start-up for the run",[69,631],{"x":134,"y":632,"width":633,"height":634,"rx":92,"fill":75,"stroke":94,"strokeWidth":95},"186","620","44",[77,636,639],{"x":637,"y":638,"textAnchor":81,"fontSize":111,"fill":84},"490","213","Per-test isolation comes from the transaction, not from the container.",[169,641,642],{},"Isolation and lifetime are independent choices. Session-scoped containers with per-test transactions give the isolation of the left-hand timeline at the cost of the right-hand one.",[38,644,646],{"id":645},"data-that-says-what-the-test-means","Data that says what the test means",[10,648,649],{},"The second-largest source of unreadable integration tests is setup. Twenty lines constructing an order, a customer, three line items and a payment before one line of assertion leaves the reader unable to see which of those twenty-one values the test is actually about.",[10,651,652],{},"Factories fix this by giving every field a sensible default and letting the test override only what matters:",[216,654,656],{"className":218,"code":655,"language":220,"meta":221,"style":221},"import factory\nfrom myapp.models import Customer, Order\n\n\nclass CustomerFactory(factory.Factory):\n    class Meta:\n        model = Customer\n\n    name = factory.Sequence(lambda n: f\"customer-{n}\")   # unique, deterministic\n    country = \"GB\"\n    vat_exempt = False\n\n\ndef test_vat_is_zero_for_exempt_customers(db_session):\n    # One overridden field: the reader sees immediately what this test is about.\n    order = Order(customer=CustomerFactory(vat_exempt=True), net=100_00)\n    assert order.vat() == 0\n",[207,657,658,663,668,672,676,681,686,691,695,700,705,710,714,718,723,728,733],{"__ignoreMap":221},[225,659,660],{"class":227,"line":228},[225,661,662],{},"import factory\n",[225,664,665],{"class":227,"line":234},[225,666,667],{},"from myapp.models import Customer, Order\n",[225,669,670],{"class":227,"line":240},[225,671,250],{"emptyLinePlaceholder":249},[225,673,674],{"class":227,"line":246},[225,675,250],{"emptyLinePlaceholder":249},[225,677,678],{"class":227,"line":253},[225,679,680],{},"class CustomerFactory(factory.Factory):\n",[225,682,683],{"class":227,"line":258},[225,684,685],{},"    class Meta:\n",[225,687,688],{"class":227,"line":264},[225,689,690],{},"        model = Customer\n",[225,692,693],{"class":227,"line":270},[225,694,250],{"emptyLinePlaceholder":249},[225,696,697],{"class":227,"line":276},[225,698,699],{},"    name = factory.Sequence(lambda n: f\"customer-{n}\")   # unique, deterministic\n",[225,701,702],{"class":227,"line":282},[225,703,704],{},"    country = \"GB\"\n",[225,706,707],{"class":227,"line":288},[225,708,709],{},"    vat_exempt = False\n",[225,711,712],{"class":227,"line":293},[225,713,250],{"emptyLinePlaceholder":249},[225,715,716],{"class":227,"line":298},[225,717,250],{"emptyLinePlaceholder":249},[225,719,720],{"class":227,"line":304},[225,721,722],{},"def test_vat_is_zero_for_exempt_customers(db_session):\n",[225,724,725],{"class":227,"line":310},[225,726,727],{},"    # One overridden field: the reader sees immediately what this test is about.\n",[225,729,730],{"class":227,"line":316},[225,731,732],{},"    order = Order(customer=CustomerFactory(vat_exempt=True), net=100_00)\n",[225,734,735],{"class":227,"line":322},[225,736,737],{},"    assert order.vat() == 0\n",[10,739,740,743,744,747,748,752,753,757],{},[207,741,742],{},"factory.Sequence"," rather than ",[207,745,746],{},"faker.name()"," for anything that must be unique is the detail that keeps factories deterministic. Random data in a factory means a test that fails one run in a thousand when the generator happens to produce a duplicate or an apostrophe — and reproducing it requires the seed, which nobody recorded. Where realistic data genuinely helps, seed the generator explicitly; ",[17,749,751],{"href":750},"\u002Fintegration-database-and-service-testing\u002Ftest-data-factories-and-builders\u002Fgenerating-reproducible-fake-data-with-faker\u002F","generating reproducible fake data with Faker"," covers the seeding hooks, and ",[17,754,756],{"href":755},"\u002Fproperty-based-fuzz-testing-strategies\u002F","property-based testing"," is the right tool when the goal is genuinely to explore the input space rather than to fill in a field.",[38,759,761],{"id":760},"contracts-not-staging-environments","Contracts, not staging environments",[10,763,764,765,768],{},"Testing against a shared staging environment produces a suite whose result depends on what somebody else deployed twenty minutes ago. The alternative is to test the ",[484,766,767],{},"agreement"," rather than the deployment: record the request and response shapes your code depends on, verify your side against them locally, and hand the provider a machine-checkable description they can verify in their own pipeline.",[10,770,771,772,776,777,179],{},"Two mechanisms cover most cases. Schema validation asserts that a real response conforms to the provider's published OpenAPI document, which catches a field that changed type without anyone's tests needing to coordinate. Consumer-driven contracts go further: the consumer's expectations are published as a pact that the provider's pipeline replays against its real implementation, so a breaking change fails the provider's build. Neither replaces the occasional end-to-end smoke test, but both move the failure to the moment and the repository where it can be fixed cheaply — the details are in ",[17,773,775],{"href":774},"\u002Fintegration-database-and-service-testing\u002Fcontract-testing-for-http-apis\u002Fvalidating-responses-against-an-openapi-schema\u002F","validating responses against an OpenAPI schema"," and ",[17,778,780],{"href":779},"\u002Fintegration-database-and-service-testing\u002Fcontract-testing-for-http-apis\u002Fconsumer-driven-contract-tests-with-pact-python\u002F","consumer-driven contract tests with Pact",[38,782,784],{"id":783},"brokers-queues-and-the-assertions-worth-making","Brokers, queues and the assertions worth making",[10,786,787],{},"Message-driven systems attract a particular kind of useless test: publish a message, sleep, assert the handler ran. It is slow, it is flaky, and it verifies the broker rather than the code. The assertions that earn their runtime are about the guarantees the broker exists to provide.",[10,789,790,793],{},[190,791,792],{},"Acknowledgement."," Does the consumer acknowledge only after the work is durable? The test is to make the handler raise, then assert the message is redelivered rather than lost. With RabbitMQ that means asserting the message reappears after a channel close; with Kafka it means asserting the offset was not committed.",[10,795,796,799],{},[190,797,798],{},"Idempotency."," Every at-least-once broker will eventually deliver the same message twice. The test is to call the handler twice with the same payload and assert the second call is a no-op — no duplicate row, no second charge, no second email.",[10,801,802,805],{},[190,803,804],{},"Ordering."," Where ordering is guaranteed per key, the test publishes an interleaved sequence across two keys and asserts per-key order is preserved while cross-key order is not assumed.",[216,807,809],{"className":218,"code":808,"language":220,"meta":221,"style":221},"import pytest\n\n\ndef test_handler_is_idempotent(db_session, order_created_event):\n    # At-least-once delivery is a given; the handler must absorb the repeat.\n    handle_order_created(order_created_event, db_session)\n    handle_order_created(order_created_event, db_session)     # same message id\n\n    orders = db_session.query(Order).filter_by(external_id=order_created_event.id).all()\n    assert len(orders) == 1          # the dedupe key did its job\n\n\ndef test_failed_handler_does_not_acknowledge(consumer, broken_handler):\n    consumer.register(broken_handler)\n    with pytest.raises(RuntimeError):\n        consumer.process_next()\n\n    # The message must still be pending for redelivery, not silently dropped.\n    assert consumer.unacknowledged_count == 1\n",[207,810,811,815,819,823,828,833,838,843,847,852,857,861,865,870,875,880,885,889,894],{"__ignoreMap":221},[225,812,813],{"class":227,"line":228},[225,814,231],{},[225,816,817],{"class":227,"line":234},[225,818,250],{"emptyLinePlaceholder":249},[225,820,821],{"class":227,"line":240},[225,822,250],{"emptyLinePlaceholder":249},[225,824,825],{"class":227,"line":246},[225,826,827],{},"def test_handler_is_idempotent(db_session, order_created_event):\n",[225,829,830],{"class":227,"line":253},[225,831,832],{},"    # At-least-once delivery is a given; the handler must absorb the repeat.\n",[225,834,835],{"class":227,"line":258},[225,836,837],{},"    handle_order_created(order_created_event, db_session)\n",[225,839,840],{"class":227,"line":264},[225,841,842],{},"    handle_order_created(order_created_event, db_session)     # same message id\n",[225,844,845],{"class":227,"line":270},[225,846,250],{"emptyLinePlaceholder":249},[225,848,849],{"class":227,"line":276},[225,850,851],{},"    orders = db_session.query(Order).filter_by(external_id=order_created_event.id).all()\n",[225,853,854],{"class":227,"line":282},[225,855,856],{},"    assert len(orders) == 1          # the dedupe key did its job\n",[225,858,859],{"class":227,"line":288},[225,860,250],{"emptyLinePlaceholder":249},[225,862,863],{"class":227,"line":293},[225,864,250],{"emptyLinePlaceholder":249},[225,866,867],{"class":227,"line":298},[225,868,869],{},"def test_failed_handler_does_not_acknowledge(consumer, broken_handler):\n",[225,871,872],{"class":227,"line":304},[225,873,874],{},"    consumer.register(broken_handler)\n",[225,876,877],{"class":227,"line":310},[225,878,879],{},"    with pytest.raises(RuntimeError):\n",[225,881,882],{"class":227,"line":316},[225,883,884],{},"        consumer.process_next()\n",[225,886,887],{"class":227,"line":322},[225,888,250],{"emptyLinePlaceholder":249},[225,890,891],{"class":227,"line":328},[225,892,893],{},"    # The message must still be pending for redelivery, not silently dropped.\n",[225,895,896],{"class":227,"line":334},[225,897,898],{},"    assert consumer.unacknowledged_count == 1\n",[10,900,901,902,905],{},"Note that neither test needs a running broker. Acknowledgement and idempotency are properties of ",[484,903,904],{},"your"," handler, testable with an in-memory consumer double, and that is where they belong. Reserve the containerised broker for the small number of tests that genuinely exercise the client library's protocol handling — connection recovery, prefetch limits, consumer-group rebalancing — because those behaviours live in the broker and the client, not in your handler.",[10,907,908,909,912],{},"The awkward middle case is the handler that does both: consumes a message, writes to the database, publishes a follow-up. That pair of side effects is the classic dual-write problem, and the test worth writing is the one that kills the process between the two. In practice that means asserting the outbox row and the database row land in the same transaction, which is an assertion the ",[17,910,911],{"href":19},"transactional fixture"," above makes trivial: roll back, and neither should exist.",[38,914,916],{"id":915},"running-integration-tests-in-ci-without-a-flaky-pipeline","Running integration tests in CI without a flaky pipeline",[10,918,919],{},"The operational half of this section is arranging for these tests to run somewhere other than a developer's laptop without becoming the reason nobody trusts the build.",[10,921,922,925],{},[190,923,924],{},"Separate them by marker, not by directory alone."," A marker lets the fast suite run on every push and the full suite run on merge, without maintaining two invocations that drift apart:",[216,927,931],{"className":928,"code":929,"language":930,"meta":221,"style":221},"language-toml shiki shiki-themes github-light github-dark","# pyproject.toml\n[tool.pytest.ini_options]\nmarkers = [\n    \"integration: needs a real database or containerised service\",\n]\naddopts = \"--strict-markers\"\n","toml",[207,932,933,938,943,948,953,958],{"__ignoreMap":221},[225,934,935],{"class":227,"line":228},[225,936,937],{},"# pyproject.toml\n",[225,939,940],{"class":227,"line":234},[225,941,942],{},"[tool.pytest.ini_options]\n",[225,944,945],{"class":227,"line":240},[225,946,947],{},"markers = [\n",[225,949,950],{"class":227,"line":246},[225,951,952],{},"    \"integration: needs a real database or containerised service\",\n",[225,954,955],{"class":227,"line":253},[225,956,957],{},"]\n",[225,959,960],{"class":227,"line":258},[225,961,962],{},"addopts = \"--strict-markers\"\n",[216,964,968],{"className":965,"code":966,"language":967,"meta":221,"style":221},"language-bash shiki shiki-themes github-light github-dark","pytest -m \"not integration\"      # pull-request feedback in under a minute\npytest -m integration            # merge queue, nightly, or on demand\n","bash",[207,969,970,988],{"__ignoreMap":221},[225,971,972,976,980,984],{"class":227,"line":228},[225,973,975],{"class":974},"sScJk","pytest",[225,977,979],{"class":978},"sj4cs"," -m",[225,981,983],{"class":982},"sZZnC"," \"not integration\"",[225,985,987],{"class":986},"sJ8bj","      # pull-request feedback in under a minute\n",[225,989,990,992,994,997],{"class":227,"line":234},[225,991,975],{"class":974},[225,993,979],{"class":978},[225,995,996],{"class":982}," integration",[225,998,999],{"class":986},"            # merge queue, nightly, or on demand\n",[10,1001,1002,1005,1006,1009,1010,179],{},[207,1003,1004],{},"--strict-markers"," matters more than it looks: without it, a typo in ",[207,1007,1008],{},"@pytest.mark.integraton"," creates a new marker silently, and those tests then run in the fast suite forever. The wider case for strict configuration is made in ",[17,1011,1013],{"href":1012},"\u002Fadvanced-pytest-architecture-configuration\u002Fpytest-configuration-best-practices\u002F","pytest configuration best practices",[10,1015,1016,1019,1020,1023],{},[190,1017,1018],{},"Give every worker its own schema."," Under ",[207,1021,1022],{},"pytest-xdist",", derive the database name from the worker id so parallel workers never share mutable state:",[216,1025,1027],{"className":218,"code":1026,"language":220,"meta":221,"style":221},"import os\n\nimport pytest\n\n\n@pytest.fixture(scope=\"session\")\ndef database_name():\n    # \"gw0\", \"gw1\", … under xdist; \"master\" when running serially.\n    worker = os.environ.get(\"PYTEST_XDIST_WORKER\", \"master\")\n    return f\"test_{worker}\"\n",[207,1028,1029,1034,1038,1042,1046,1050,1054,1059,1064,1069],{"__ignoreMap":221},[225,1030,1031],{"class":227,"line":228},[225,1032,1033],{},"import os\n",[225,1035,1036],{"class":227,"line":234},[225,1037,250],{"emptyLinePlaceholder":249},[225,1039,1040],{"class":227,"line":240},[225,1041,231],{},[225,1043,1044],{"class":227,"line":246},[225,1045,250],{"emptyLinePlaceholder":249},[225,1047,1048],{"class":227,"line":253},[225,1049,250],{"emptyLinePlaceholder":249},[225,1051,1052],{"class":227,"line":258},[225,1053,261],{},[225,1055,1056],{"class":227,"line":264},[225,1057,1058],{},"def database_name():\n",[225,1060,1061],{"class":227,"line":270},[225,1062,1063],{},"    # \"gw0\", \"gw1\", … under xdist; \"master\" when running serially.\n",[225,1065,1066],{"class":227,"line":276},[225,1067,1068],{},"    worker = os.environ.get(\"PYTEST_XDIST_WORKER\", \"master\")\n",[225,1070,1071],{"class":227,"line":282},[225,1072,1073],{},"    return f\"test_{worker}\"\n",[10,1075,1076,1079,1080,1083],{},[190,1077,1078],{},"Cache the images, not the data."," Docker layer caching on the runner removes the image pull, which is usually the largest fixed cost. Caching a ",[484,1081,1082],{},"populated"," database volume between runs, by contrast, reintroduces exactly the order dependence transactions were removed to avoid.",[10,1085,1086,1089,1090,179],{},[190,1087,1088],{},"Make failures self-describing."," An integration failure in CI is expensive to reproduce, so the run should hand you everything needed: the container logs, the last SQL statements, and the test's own captured output. Wiring that up once is the difference between a ten-minute diagnosis and a day of re-running the job with extra prints — the mechanics are in ",[17,1091,1093],{"href":1092},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-tests-in-ci-and-containers\u002Fcapturing-artifacts-from-a-failed-ci-test-run\u002F","capturing artifacts from a failed CI test run",[46,1095,1097,1213],{"className":1096},[49],[51,1098,59,1103,59,1106,59,1109,59,1125,59,1129,59,1133,59,1139,59,1144,59,1148,59,1152,59,1156,59,1160,59,1166,59,1168,59,1171,59,1174,59,1177,59,1179,59,1182,59,1186,59,1190,59,1194,59,1197,59,1200,59,1202,59,1205,59,1209],{"viewBox":1099,"role":54,"ariaLabelledBy":1100,"xmlns":58},"0 0 800 244",[1101,1102],"cisplit-t","cisplit-d",[61,1104,1105],{"id":1101},"Splitting fast and integration suites across pipeline stages",[65,1107,1108],{"id":1102},"A pipeline with three stages. Every push runs the fast suite excluding integration-marked tests in about one minute. The merge queue runs the integration suite against session-scoped containers in about six minutes. A nightly stage runs the full suite plus contract verification, and each stage lists what a failure there tells you.",[1110,1111,1112,1113,59],"defs",{},"\n    ",[1114,1115,1121],"marker",{"id":1116,"viewBox":1117,"refX":1118,"refY":1119,"markerWidth":535,"markerHeight":535,"orient":1120},"cisplit-a","0 0 10 10","9","5","auto-start-reverse",[1122,1123],"path",{"d":1124,"fill":84},"M0 0 L10 5 L0 10 z",[69,1126],{"x":71,"y":71,"width":1127,"height":1128,"rx":74,"fill":75},"800","244",[77,1130,1132],{"x":1131,"y":80,"textAnchor":81,"fontSize":82,"fontWeight":83,"fill":84},"400","Which suite runs where",[69,1134],{"x":1135,"y":1136,"width":143,"height":1137,"rx":111,"fill":117,"stroke":118,"strokeWidth":1138},"24","52","120","2",[77,1140,1143],{"x":1141,"y":1142,"textAnchor":81,"fontSize":99,"fontWeight":83,"fill":84},"136","78","every push",[77,1145,1147],{"x":1141,"y":1146,"textAnchor":81,"fontSize":104,"fill":84},"100","pytest -m \"not integration\"",[77,1149,1151],{"x":1141,"y":1150,"textAnchor":81,"fontSize":104,"fill":112},"122","~1 minute",[77,1153,1155],{"x":1141,"y":1154,"textAnchor":81,"fontSize":104,"fill":84},"146","a failure means",[77,1157,1159],{"x":1141,"y":1158,"textAnchor":81,"fontSize":104,"fill":84},"162","your logic broke",[227,1161],{"x1":1162,"y1":1163,"x2":1164,"y2":1163,"stroke":84,"strokeWidth":536,"markerEnd":1165},"252","112","286","url(#cisplit-a)",[69,1167],{"x":595,"y":1136,"width":143,"height":1137,"rx":111,"fill":135,"stroke":136,"strokeWidth":1138},[77,1169,1170],{"x":610,"y":1142,"textAnchor":81,"fontSize":99,"fontWeight":83,"fill":84},"merge queue",[77,1172,1173],{"x":610,"y":1146,"textAnchor":81,"fontSize":104,"fill":84},"pytest -m integration",[77,1175,1176],{"x":610,"y":1150,"textAnchor":81,"fontSize":104,"fill":148},"~6 minutes",[77,1178,1155],{"x":610,"y":1154,"textAnchor":81,"fontSize":104,"fill":84},[77,1180,1181],{"x":610,"y":1158,"textAnchor":81,"fontSize":104,"fill":84},"a boundary broke",[227,1183],{"x1":1184,"y1":1163,"x2":1185,"y2":1163,"stroke":84,"strokeWidth":536,"markerEnd":1165},"520","554",[69,1187],{"x":1188,"y":1136,"width":1189,"height":1137,"rx":111,"fill":153,"stroke":154,"strokeWidth":1138},"560","216",[77,1191,1193],{"x":1192,"y":1142,"textAnchor":81,"fontSize":99,"fontWeight":83,"fill":84},"668","nightly",[77,1195,1196],{"x":1192,"y":1146,"textAnchor":81,"fontSize":104,"fill":84},"full suite + contracts",[77,1198,1199],{"x":1192,"y":1150,"textAnchor":81,"fontSize":104,"fill":166},"~20 minutes",[77,1201,1155],{"x":1192,"y":1154,"textAnchor":81,"fontSize":104,"fill":84},[77,1203,1204],{"x":1192,"y":1158,"textAnchor":81,"fontSize":104,"fill":84},"someone else shipped",[69,1206],{"x":1135,"y":632,"width":1207,"height":1208,"rx":92,"fill":75,"stroke":94,"strokeWidth":95},"752","42",[77,1210,1212],{"x":1131,"y":1211,"textAnchor":81,"fontSize":111,"fill":84},"212","One marker, three invocations — no second test command to drift out of date.",[169,1214,1215],{},"The stages differ in what a red build tells you. Keeping that signal clean is worth more than shaving a minute off the slowest stage.",[38,1217,1219],{"id":1218},"schema-changes-are-the-riskiest-thing-to-leave-untested","Schema changes are the riskiest thing to leave untested",[10,1221,1222],{},"A migration is the one piece of code that runs exactly once against data you cannot see, in an environment you cannot roll back cheaply, at the moment of a deploy. Test suites routinely skip them, because the application's tests create their schema from the model definitions rather than by running the migrations — which means the migrations are never executed until production.",[10,1224,1225,1226,1229],{},"That gap produces two specific failures. The first is ",[484,1227,1228],{},"drift",": the models and the migration history disagree, so a fresh database built from migrations differs from the one the tests ran against. Alembic detects this directly — autogenerate a revision against a migrated database and assert that it is empty:",[216,1231,1233],{"className":218,"code":1232,"language":220,"meta":221,"style":221},"from alembic.autogenerate import compare_metadata\nfrom alembic.migration import MigrationContext\n\nfrom myapp.models import Base\n\n\ndef test_models_match_migrations(engine_migrated):\n    # engine_migrated points at a database built by running every migration.\n    with engine_migrated.connect() as connection:\n        context = MigrationContext.configure(connection)\n        diff = compare_metadata(context, Base.metadata)\n\n    assert diff == [], f\"models and migrations disagree: {diff}\"\n",[207,1234,1235,1240,1245,1249,1254,1258,1262,1267,1272,1277,1282,1287,1291],{"__ignoreMap":221},[225,1236,1237],{"class":227,"line":228},[225,1238,1239],{},"from alembic.autogenerate import compare_metadata\n",[225,1241,1242],{"class":227,"line":234},[225,1243,1244],{},"from alembic.migration import MigrationContext\n",[225,1246,1247],{"class":227,"line":240},[225,1248,250],{"emptyLinePlaceholder":249},[225,1250,1251],{"class":227,"line":246},[225,1252,1253],{},"from myapp.models import Base\n",[225,1255,1256],{"class":227,"line":253},[225,1257,250],{"emptyLinePlaceholder":249},[225,1259,1260],{"class":227,"line":258},[225,1261,250],{"emptyLinePlaceholder":249},[225,1263,1264],{"class":227,"line":264},[225,1265,1266],{},"def test_models_match_migrations(engine_migrated):\n",[225,1268,1269],{"class":227,"line":270},[225,1270,1271],{},"    # engine_migrated points at a database built by running every migration.\n",[225,1273,1274],{"class":227,"line":276},[225,1275,1276],{},"    with engine_migrated.connect() as connection:\n",[225,1278,1279],{"class":227,"line":282},[225,1280,1281],{},"        context = MigrationContext.configure(connection)\n",[225,1283,1284],{"class":227,"line":288},[225,1285,1286],{},"        diff = compare_metadata(context, Base.metadata)\n",[225,1288,1289],{"class":227,"line":293},[225,1290,250],{"emptyLinePlaceholder":249},[225,1292,1293],{"class":227,"line":298},[225,1294,1295],{},"    assert diff == [], f\"models and migrations disagree: {diff}\"\n",[10,1297,1298,1299,1302,1303,1306,1307,179],{},"The second failure is ",[484,1300,1301],{},"data",": a migration that works on an empty schema and fails on real rows, because a new ",[207,1304,1305],{},"NOT NULL"," column has no default, or a backfill loop times out on a table with fifty million rows. The test is to apply the migration to a database that already contains representative rows, which the session-scoped container makes cheap. Both checks, plus the downgrade path that most teams discover is broken at the worst possible moment, are covered in ",[17,1308,1310],{"href":1309},"\u002Fintegration-database-and-service-testing\u002Fdatabase-fixtures-and-transactional-tests\u002Ftesting-alembic-migrations-in-ci\u002F","testing Alembic migrations in CI",[10,1312,1313,1314,1317],{},"Building the test database from migrations rather than from ",[207,1315,1316],{},"metadata.create_all()"," has one more benefit worth the switch on its own: it makes every test run an implicit smoke test of the migration history. If a revision is unapplyable, the suite cannot start, and the failure arrives at the pull request rather than at the deploy.",[38,1319,1321],{"id":1320},"common-pitfalls-and-antipatterns","Common pitfalls and antipatterns",[1323,1324,1325,1336,1347,1357,1366,1376],"ol",{},[1326,1327,1328,1331,1332,1335],"li",{},[190,1329,1330],{},"Seeding through the application's own API."," Creating a user by calling ",[207,1333,1334],{},"POST \u002Fusers"," couples every test to the endpoint's current behaviour and makes an unrelated validation change break two hundred tests. Root cause: setup as a test. Fix: insert through the model or factory layer directly.",[1326,1337,1338,1019,1341,1343,1344,179],{},[190,1339,1340],{},"Sharing a database between parallel workers.",[207,1342,1022],{},", two workers truncating the same tables produce failures that look like race conditions in your code. Root cause: shared mutable state across processes. Fix: one schema or one database per worker, keyed on ",[207,1345,1346],{},"PYTEST_XDIST_WORKER",[1326,1348,1349,1352,1353,1356],{},[190,1350,1351],{},"Asserting on auto-increment identifiers."," ",[207,1354,1355],{},"assert order.id == 1"," passes exactly once. Root cause: an assertion about the database's internal counter. Fix: assert on values the test supplied, or on relationships.",[1326,1358,1359,1352,1362,1365],{},[190,1360,1361],{},"Unpinned container images.",[207,1363,1364],{},"postgres:latest"," makes the suite's behaviour a function of the CI cache. Root cause: an implicit dependency. Fix: pin the tag, and upgrade it deliberately in its own change.",[1326,1367,1368,1375],{},[190,1369,1370,1371,1374],{},"Cleaning up with ",[207,1372,1373],{},"TRUNCATE ... CASCADE"," in a session fixture."," It silently removes reference data the suite seeded once, so tests pass in isolation and fail in bulk. Root cause: cleanup scoped wider than the data it owns. Fix: roll back per test; reserve truncation for a deliberate reset between test groups.",[1326,1377,1378,1381,1382,179],{},[190,1379,1380],{},"Integration tests with no timeout."," A container that never becomes ready turns into a job that runs until the CI platform kills it, with no traceback. Root cause: unbounded waiting. Fix: a wait strategy with an explicit timeout, plus a suite-wide ceiling via ",[17,1383,1385],{"href":1384},"\u002Ftesting-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002Ffailing-fast-with-pytest-timeout\u002F","pytest-timeout",[38,1387,1389],{"id":1388},"frequently-asked-questions","Frequently Asked Questions",[10,1391,1392,1395],{},[190,1393,1394],{},"Should integration tests share one database or get one each?","\nOne database, many transactions. Starting a database per test costs seconds each and exhausts container resources; starting one per session and wrapping every test in a transaction that rolls back gives the same isolation for a few milliseconds. Use separate databases only when a test needs DDL that cannot run inside a transaction, or when parallel workers must not see each other's schema changes.",[10,1397,1398,1401,1402,1405],{},[190,1399,1400],{},"How do I stop integration tests from being order-dependent?","\nRoll back rather than delete, seed reference data in a session fixture that never changes, and never let a test rely on an identifier produced by an earlier test. Then prove it by running with ",[207,1403,1404],{},"pytest-randomly","; an order-dependent suite fails within a handful of seeds, and the bisection workflow narrows it to the offending pair.",[10,1407,1408,1411],{},[190,1409,1410],{},"Are Testcontainers too slow for CI?","\nOnly if started per test. A session-scoped container costs one image pull, cached by the runner, plus two to five seconds of startup amortised across the whole suite. What makes them slow is polling readiness with sleeps instead of a real wait strategy, and pulling images without a registry cache.",[10,1413,1414,1417],{},[190,1415,1416],{},"What belongs in an integration test rather than a unit test?","\nAnything whose behaviour lives outside your code: SQL semantics, constraint violations, transaction isolation, migration ordering, serialization formats on the wire, and authentication handshakes. Business rules above those boundaries belong in fast tests with in-memory fakes.",[10,1419,1420,1423],{},[190,1421,1422],{},"How do contract tests differ from integration tests against a real service?","\nAn integration test proves your code works against the version of the service running right now. A contract test proves the agreement itself — request shape, response shape, status codes — and can be verified by the provider in their own pipeline, so a breaking change is caught by whoever made it rather than by whoever consumes it.",[38,1425,1427],{"id":1426},"related-guides","Related guides",[1429,1430,1431,1437,1443,1449,1454],"ul",{},[1326,1432,1433,1434,179],{},"Build the fixture layer first with ",[17,1435,1436],{"href":19},"database fixtures and transactional tests",[1326,1438,1439,1440,179],{},"Get real services into the suite with ",[17,1441,1442],{"href":24},"spinning up services with Testcontainers",[1326,1444,1445,1446,179],{},"Make preconditions readable using ",[17,1447,1448],{"href":29},"test data factories and builders",[1326,1450,1451,1452,179],{},"Move breakage detection upstream with ",[17,1453,35],{"href":34},[1326,1455,1456,1457,1461,1462,179],{},"Keep the fast layers fast by faking at the transport in ",[17,1458,1460],{"href":1459},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002F","mocking network and HTTP calls",", and keep async integration fixtures on one loop with ",[17,1463,1465],{"href":1464},"\u002Ftesting-async-and-concurrent-python\u002F","testing async and concurrent Python",[10,1467,1468,1469],{},"← Back to ",[17,1470,1472],{"href":1471},"\u002F","all guides",[1474,1475,1476],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":221,"searchDepth":234,"depth":234,"links":1478},[1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489],{"id":40,"depth":234,"text":41},{"id":182,"depth":234,"text":183},{"id":393,"depth":234,"text":394},{"id":645,"depth":234,"text":646},{"id":760,"depth":234,"text":761},{"id":783,"depth":234,"text":784},{"id":915,"depth":234,"text":916},{"id":1218,"depth":234,"text":1219},{"id":1320,"depth":234,"text":1321},{"id":1388,"depth":234,"text":1389},{"id":1426,"depth":234,"text":1427},"Run tests against real Postgres, Redis and HTTP services without flakiness: transactional fixtures, Testcontainers lifecycles, data factories, and contract verification.","md",{"slug":1493,"type":1494,"breadcrumb":1495,"datePublished":1496,"dateModified":1496,"faq":1497},"integration-database-and-service-testing","guide","Integration & Services","2026-09-18",[1498,1500,1502,1504,1506],{"q":1394,"a":1499},"One database, many transactions. Starting a database per test costs seconds each and exhausts container resources; starting one per session and wrapping every test in a transaction that rolls back gives the same isolation for a few milliseconds. Use separate databases only when a test needs DDL that cannot run inside a transaction, or when parallel workers must not see each other's schema changes.",{"q":1400,"a":1501},"Roll back rather than delete, seed reference data in a session fixture that never changes, and never let a test rely on an identifier produced by an earlier test. Then prove it by running with pytest-randomly; an order-dependent suite fails within a handful of seeds, and the bisection workflow narrows it to the offending pair.",{"q":1410,"a":1503},"Only if started per test. A session-scoped container costs one image pull, cached by the runner, plus two to five seconds of startup amortised across the whole suite. What makes them slow is polling readiness with sleeps instead of a real wait strategy, and pulling images without a registry cache.",{"q":1416,"a":1505},"Anything whose behaviour lives outside your code: SQL semantics, constraint violations, transaction isolation, migration ordering, serialization formats on the wire, and authentication handshakes. Business rules above those boundaries belong in fast tests with in-memory fakes.",{"q":1422,"a":1507},"An integration test proves your code works against the version of the service running right now. A contract test proves the agreement itself — request shape, response shape, status codes — and can be verified by the provider in their own pipeline, so a breaking change is caught by whoever made it rather than by whoever consumes it.","\u002Fintegration-database-and-service-testing",{"title":5,"description":1490},"integration-database-and-service-testing\u002Findex","8uyXqjyDEG4FZbfUnRTH3oHvDD_5SGczkVaibZHBPJ4",1789718765717]