{"post":{"slug":"integration-testing-explained-2026","title":"Integration testing explained in 2026: test the seams that unit tests miss","excerpt":"Integration tests catch the failures that happen when two correct pieces connect incorrectly. Here is the 2026 guide to writing fast, reliable integration tests.","category":"code","date":"April 6, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">I</span>ntegration tests live in the middle of the testing pyramid: slower than unit tests, faster and cheaper than end-to-end tests, and essential for catching the failure modes unit tests structurally cannot see. Your user service may have perfect unit test coverage, and it will still fail in production if the SQL query it generates violates a database constraint. Integration tests are how you find that before deployment.\n\n## What changed in 2026\n\n- **Testcontainers became the standard** for spinning up real databases in CI. The Java, Python, Go, and Node.js clients are all stable and fast (~3–5 s to start a Postgres container).\n- **Docker Compose v2** (now the default) simplified multi-service test environments with `--wait` for health checks.\n- **Vitest and pytest-asyncio** make writing async integration tests as simple as unit tests.\n- **GitHub Actions caching** of container images cut integration test CI time by 40–60% for most teams.\n\n## What integration tests cover\n\n| Scenario | Why unit tests miss it |\n| --- | --- |\n| SQL query correctness | Unit tests mock the database |\n| HTTP client + real API contract | Unit tests stub the response |\n| Cache hit/miss logic with Redis | Unit tests mock the cache layer |\n| Message queue publish/consume | Unit tests mock the broker |\n| ORM migrations + constraints | Unit tests use in-memory fakes |\n\nAnywhere two components connect is a seam. Integration tests verify seams.\n\n## A minimal Postgres integration test (Node.js + Vitest)\n\n```ts\n// order.repository.integration.test.ts\nimport { describe, it, expect, beforeAll, afterAll } from \"vitest\";\nimport { GenericContainer, StartedTestContainer } from \"testcontainers\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { Pool } from \"pg\";\nimport { OrderRepository } from \"./order.repository\";\n\nlet container: StartedTestContainer;\nlet repo: OrderRepository;\n\nbeforeAll(async () => {\n  container = await new GenericContainer(\"postgres:16\")\n    .withEnvironment({ POSTGRES_PASSWORD: \"test\", POSTGRES_DB: \"testdb\" })\n    .withExposedPorts(5432)\n    .start();\n\n  const pool = new Pool({\n    host: container.getHost(),\n    port: container.getMappedPort(5432),\n    database: \"testdb\",\n    password: \"test\",\n    user: \"postgres\",\n  });\n  repo = new OrderRepository(drizzle(pool));\n  await runMigrations(pool); // apply schema\n}, 30_000);\n\nafterAll(() => container.stop());\n\nit(\"saves and retrieves an order\", async () => {\n  const order = { id: \"ord-1\", amount: 99 };\n  await repo.save(order);\n  const found = await repo.findById(\"ord-1\");\n  expect(found).toMatchObject(order);\n});\n```\n\n## Test isolation strategies\n\n| Strategy | How | Best for |\n| --- | --- | --- |\n| Wrap in transaction, rollback | Begin transaction in beforeEach, rollback in afterEach | Fast, no cleanup code |\n| Truncate tables | DELETE all rows in afterEach | Works across multiple connections |\n| Fresh schema per test | Create a schema per test, drop after | Parallel tests with no collision |\n| Separate database per test run | Each CI job gets its own DB | Maximum isolation, needs provisioning |\n\nTransaction rollback is the fastest and most common approach for PostgreSQL/MySQL. It does not work across services that use separate connections (e.g., testing a service that spawns a worker process).\n\n## Python example with pytest + Testcontainers\n\n```python\n# test_user_repository.py\nimport pytest\nfrom testcontainers.postgres import PostgresContainer\nfrom myapp.db import create_engine, run_migrations\nfrom myapp.repositories import UserRepository\n\n@pytest.fixture(scope=\"session\")\ndef pg():\n    with PostgresContainer(\"postgres:16\") as pg:\n        yield pg\n\n@pytest.fixture\ndef repo(pg):\n    engine = create_engine(pg.get_connection_url())\n    run_migrations(engine)\n    return UserRepository(engine)\n\ndef test_find_user_by_email(repo):\n    repo.create(email=\"a@example.com\", name=\"Alice\")\n    user = repo.find_by_email(\"a@example.com\")\n    assert user.name == \"Alice\"\n```\n\n## How to pick\n\n1. **Service with database queries** — write integration tests for every non-trivial query.\n2. **HTTP client wrapping a third-party API** — use WireMock or `msw` to record/replay real responses.\n3. **Message queue publish/consume** — Testcontainers has first-class support for RabbitMQ and Kafka.\n4. **Pure business logic** — stick to unit tests; integration tests add cost with no benefit here.\n\n## Common mistakes\n\n**Shared mutable test data.** Tests that depend on the order they run or the presence of rows left by another test are a reliability disaster. Isolate.\n\n**Testing too much in one test.** An integration test that creates 5 objects, makes 3 service calls, and asserts 10 things is a debugging maze. Keep the scope narrow.\n\n**Slow setup not cached.** Starting a Postgres container for every test function is needlessly slow. Use session-scoped or suite-scoped fixtures.\n\n**Skipping integration tests in CI.** Unit tests plus no integration tests means bugs ship. At minimum run the integration suite on every merge to main.\n\n## What to skip\n\n- **Full application startup for a repository test** — you do not need an HTTP server to test a database query.\n- **Real external API calls in CI** — use WireMock, VCR, or `msw` to record once and replay; external APIs are slow, flaky, and have rate limits.\n- **Writing integration tests for every unit** — only test the seams; leave logic testing to unit tests.\n\n## FAQ\n\n**How long should integration tests take?**\nAim for under 30 seconds for the full integration suite. If it exceeds 60 seconds, look for redundant container starts or overly broad test scope.\n\n**Can integration tests run in parallel?**\nYes, with proper isolation. Use separate schemas or databases per parallel worker; never share state.\n\n**Should I mock the database in integration tests?**\nNo. If you mock the database, it is a unit test. Use a real database — Testcontainers makes this cheap.\n\n**Where do integration tests live in the repo?**\nCommon conventions: `tests/integration/` (separate directory), or alongside source files with a `.integration.test.ts` suffix. Keep them separate so you can run just unit tests locally.\n\n## Where to go next\n\nSee [Unit testing explained in 2026](/blog/unit-testing-explained-2026), [Mocking explained in 2026](/blog/mocking-explained-2026), and [How to write a Dockerfile in 2026](/blog/how-to-write-a-dockerfile-2026).","hue":305,"metaTitle":"Integration Testing Explained 2026","metaDescription":"2026 guide to integration testing. What integration tests cover, how to use Testcontainers and Docker Compose, test isolation strategies, and what to skip.","keywords":["integration testing explained 2026","how to write integration tests 2026","testcontainers 2026","docker compose testing","database integration tests","test isolation strategies","integration vs unit tests","api integration testing","integration test best practices","supertest express testing"],"tldr":["**Integration tests verify that components work together** — database, cache, external API, message queue.","**Use real infrastructure in tests** — Docker Compose or Testcontainers give you a real Postgres in seconds.","**Isolate each test with transactions or table truncations** — never let test data bleed between tests.","**Keep integration tests in a separate suite** — run them in CI but not on every file save.","**Skip:** spinning up your full stack for an integration test — test one seam at a time."],"readTime":"4 min read","wordCount":922},"related":[{"slug":"unit-testing-explained-2026","title":"Unit testing explained in 2026: write tests that actually help","excerpt":"Most developers know they should write unit tests but struggle with what to test, how to structure them, and why their tests keep breaking. Here is the 2026 guide.","category":"code","date":"March 19, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":322},{"slug":"unit-vs-integration-testing-2026","title":"Unit vs Integration Testing Explained 2026: Where Each Pays Off","excerpt":"Unit tests check one piece in isolation; integration tests check pieces working together. Here is the 2026 guide to the difference, the right balance, and how to avoid slow, brittle test suites.","category":"code","date":"July 1, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":150},{"slug":"how-to-write-integration-tests-2026","title":"How to write integration tests in 2026","excerpt":"Integration tests catch the bugs unit tests miss by exercising real component boundaries. Here is a practical 2026 guide with patterns that actually ship.","category":"code","date":"March 2, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":305}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"indexes-explained-2026","title":"Database indexes explained in 2026: what every developer must know","excerpt":"Indexes make or break query performance. Here is the 2026 guide covering B-tree, GIN, GiST, and partial indexes with concrete examples and tradeoffs.","category":"code","date":"April 6, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":309},"older":{"slug":"ipad-air-vs-pro-2026","title":"iPad Air vs Pro in 2026: which iPad is worth the money?","excerpt":"The iPad Pro got more powerful and more expensive in 2026, while the Air remains exceptional value. Here is how to decide between them based on real use cases.","category":"tech","date":"April 6, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":228}}