Should I test that?

Should I test for race conditions?

Verdict

Yes

Yes, test each race condition the code must survive, such as two requests creating the same record, with a test against the real database that runs both checks before either write; do not add random stress tests to hunt for races nobody has named.

Why

Yes, test the race conditions you can name, against the real database where the protection lives. The typical case is a web backend with several instances, where two requests read and then write the same rows at once, such as two signups with one email. Blast radius is users, because customers see duplicate accounts, and Change frequency is regularly, because a monthly handler change can split an atomic write in two. Detectability is eventually, because the race fires only when requests overlap and leaves plausible rows. Reversibility is with-effort, because duplicates need a merge script, and Test cost is moderate, because two connections driven step by step reproduce the overlap in about an hour, so rule R11 gives Test.

When the decision changes
WhenDecisionWhy
Two clicks on Pay can send two charge requests for one orderTest mandatory: run the checks of both requests before either charge and assert one paymentBlast radius rises to money and Reversibility to costly, because a double charge needs a refund
Request threads share one service object that stores the current user in a fieldTest mandatory: interleave two users' calls on one shared instance and assert each result holds only its own user's recordsBlast radius rises to safety-or-legal and Detectability to never, because a leaked page looks normal
A unique index already refuses the duplicate row, and the losing request shows an error that users reportTest minimally: one test that the second request gets a conflict response, not a server errorDetectability falls to same-day and Reversibility to trivial, because nothing wrong is stored
A view counter on a staff dashboard sometimes loses an increment, and its code changes a few times a yearDo not test the counter; write the increment as one UPDATE statement, which the database applies atomicallyBlast radius falls to internal, Reversibility to trivial and Change frequency to rarely
Two services update one customer record from a queue, in an order that no local setup reproducesTest it differently: a nightly query compares both services' copies and alerts on a differenceTest cost rises to heavy while Detectability stays eventually and Reversibility with-effort
The shared state is a struct in a Go service whose tests already call the handlers from several goroutinesTest: run the existing tests with go test -race in CITest cost falls to trivial; Blast radius users and Detectability eventually keep the decision at Test

What breaks if you don't test

A suite that sends one request at a time never makes two requests overlap. Production does under load: a double tap over a slow connection, or a retry that arrives while the first attempt still runs. The code stores two rows where it allows one, nobody sees an error, and support hears about it weeks later from a customer whose data vanished.

What you lose if you over-test

The common over-test starts 100 threads with sleeps and asserts a count at the end. It slows CI and passes whenever the threads happen not to interleave, so a green run proves nothing. A race test for one atomic statement, such as UPDATE counters SET n = n + 1, tests PostgreSQL instead of your code.

How to test

Write integration tests on the production database engine, one per race the code must handle. Drive two connections by hand in one thread, so the order is fixed: both reads, then both writes. Under Read Committed, the PostgreSQL default, a query sees only data committed before it began, which is the window a race needs. The minimum set:

  1. For each check-then-write, one test where both checks pass before either write, asserting that the database refuses the second write.
  2. One test that the losing request gets a 409 or the existing record instead of a 500.
  3. For shared memory in Go, run the tests with go test -race, which finds races only on paths the tests execute.

When the answer changes

  • The race can charge, pay out, or refund twice.
  • A shared object holds one request's user while another request reads it.

Real incident + Code example

The double tap that made two accounts

On a project-tracking app I worked on, signup called Django's get_or_create(email=...), and the email column had no unique constraint. Django documents that concurrent calls then may insert duplicate rows. People on slow phones tapped "Create account" twice, and login picked whichever row came first, so some customers signed into an empty account. The first "my projects are gone" ticket came five weeks after launch, when 38 emails had two accounts, and merging them took two days. Then we added a unique index on lower(email) and this test, which fails without it:

import psycopg
import pytest

CHECK = "SELECT 1 FROM users WHERE lower(email) = lower(%s)"
INSERT = "INSERT INTO users (email) VALUES (%s)"


def test_two_signups_with_one_email_leave_one_account(db_url):
    # db_url: a fresh database built by the real migrations.
    # Two connections stand in for two overlapping signup requests.
    with psycopg.connect(db_url) as first, psycopg.connect(db_url) as second:
        assert first.execute(CHECK, ["ana@example.com"]).fetchone() is None
        assert second.execute(CHECK, ["Ana@example.com"]).fetchone() is None

        first.execute(INSERT, ["ana@example.com"])
        first.commit()
        with pytest.raises(psycopg.errors.UniqueViolation):
            second.execute(INSERT, ["Ana@example.com"])
        second.rollback()

FAQ

Should I unit test concurrency?

Unit test concurrency only when the shared state lives in memory, such as a cache that request threads share. Races on database rows need integration tests against the real engine, because only the engine decides which write it refuses.

Should I unit test for multithreading problems?

Yes, write a test that forces the harmful order before you add the lock, so you watch it fail and then pass. A latch between the read and the write fixes the order; threads that merely start together can pass without the lock.

Can a test prove code has no race conditions?

No, a test shows only that the orders it ran were safe. Test the harmful order of each race you know, and let database constraints and atomic statements refuse bad data in the orders you have not named.