Running go test ./... caused flaky database test failures due to concurrent test packages sharing a single MySQL test database

Problem description

I have a Go project with integration tests that use a MySQL test database. Each package has its own tests (e.g. cmd/web, internal/models), and each package sets up and tears down the database schema using setup.sql and teardown.sql.

Individually, running tests per package works fine:

  • go test ./internal/models → passes
  • go test ./cmd/web → passes

However, when running the full suite: go test ./…

I started getting intermittent failures such as:

  • Error 1050: Table 'snippets' already exists
  • teardown not consistently removing schema state
  • tests passing or failing depending on which packages were included

Initially, I assumed this was a cleanup issue in my setup/teardown logic. But after investigation, I realized the key issue:

  • Each package is compiled into a separate test binary
  • go test ./... runs multiple test binaries concurrently
  • All test packages were connecting to the same MySQL database (test_snippetbox)

This caused race conditions where:

  • one package’s setup interfered with another package’s teardown
  • schema creation happened while another package still assumed a clean DB

Running with -p=1 confirmed the issue was concurrency-related.

Solution I ended up using

I changed the test setup so that each test package gets its own isolated database, e.g.:

  • test_snippetbox_cmd_web
  • test_snippetbox_internal_models

Each package:

  • creates its own database in setup
  • runs migrations / setup.sql inside that database
  • drops the database in teardown

This removed all cross-package interference and made go test ./... fully stable even with parallel execution.

Question

Is this the recommended approach in Go for integration tests with external databases (i.e. per-package database isolation), or is there a more idiomatic pattern that is generally preferred in production Go projects?

You could try something like this where you wrap each test in a transaction then roll it back to leave the DB in a known good state for other tests: