← Engineering Notes

Integration Testing

Go and Backend Engineering · last updated Oct 2026

Concept

Integration tests exercise real components together rather than testing individual units in isolation. Where a unit test might mock a database call and verify that a function processes its return value correctly, an integration test runs the function against a real database and verifies the actual result.

Integration tests give you confidence that the pieces work together correctly: that SQL queries produce the expected results, that transactions behave as expected under concurrent access, and that HTTP handlers return the right responses end-to-end.

Problem

Unit tests with mocks can give false confidence. A unit test verifies that your code does the right thing with whatever the mock returns, but it doesn't verify that the real dependency returns what you expect, or that your SQL queries are syntactically and semantically correct.

For backend services that depend heavily on database correctness — like Simple Bank, where transaction semantics matter — unit tests with mocked databases are of limited value. They can't catch subtle query bugs, constraint violations, or incorrect isolation behaviour.

How it works

A practical approach for Go services with PostgreSQL:

  • Run a real PostgreSQL instance in tests. Docker makes this straightforward: start a container at the beginning of the test suite, run migrations to get the schema, run tests, and stop the container.
  • Use a test database separate from development and production. Migrations run fresh for each test run.
  • Wrap each test in a transaction and roll it back at the end, rather than deleting test data. This keeps tests isolated without truncating tables.

Go's testing package supports this well. The TestMain function runs setup and teardown for the entire test suite:

func TestMain(m *testing.M) {
    // set up test database
    db = setupTestDB()
    exitCode := m.Run()
    // tear down
    db.Close()
    os.Exit(exitCode)
}

For HTTP handler tests, Go's net/http/httptest package provides a test server and recorder, allowing end-to-end handler tests without starting a real server:

w := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/accounts", body)
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusCreated, w.Code)

Trade-offs

Integration tests are slower than unit tests. They involve I/O — database connections, query round-trips, container startup. A suite of integration tests may take seconds or minutes rather than milliseconds.

They also require infrastructure. Running against a real database in CI requires the CI environment to have that database available. Docker-based test environments are the usual solution.

The balance I find useful: write integration tests for database interactions, HTTP handler behaviour, and anything that requires real components. Write unit tests for pure logic — validation rules, transformation functions, business calculations — where mocks are sufficient and tests run fast.

Practical application

In Simple Bank, all database operations are tested against a real PostgreSQL instance. The tests verify that concurrent transfers don't corrupt balances, that constraint violations are caught and returned correctly, and that transaction rollbacks work as expected.

The -race flag is always used when running tests for code that uses goroutines. Go's race detector catches concurrent memory access bugs that might not cause visible failures in regular test runs.

My understanding

For database-heavy services, integration tests are more valuable than unit tests with mocked databases. A mock that returns sql.ErrNoRows tells you your code handles that error correctly. A real database test tells you the query is correct, the schema matches your assumptions, and the transaction semantics are what you expect.

The investment in setting up a real test database pays off quickly. The confidence you get from tests that exercise actual SQL, actual constraints, and actual transaction behaviour is qualitatively different from tests against mocks.

← Back to Engineering Notes