Should I test that?

Is it enough to test only the happy path?

Answer

Yes, test the unhappy paths too: in typical product code the happy path alone is not enough, so add tests for the failure branches most likely to break, such as a failed call to another service or a partial save.

Verdict on the code under testYes

Why

Test more than the happy path: typical product code needs tests for the main path and for the failure branches most likely to break. The typical case is a web application feature whose successful request already has a test. Blast radius is users and Change frequency is regularly, because the feature changes about once a month. Detectability is eventually, because error branches run rarely and a broken one often looks like normal output. Reversibility is with-effort, since records saved by a broken branch need a repair script, and Test cost is moderate, about an hour to stub a failing dependency, so rule R11 gives Test.

When the decision changes
WhenDecisionWhy
A failure branch handles a declined card or a payment provider timeoutTest mandatory: the declined, timed-out and duplicate responses, reviewed by a second personBlast radius rises to money and Reversibility to costly, because an order that ships unpaid is written off
The unhappy path is a denied request, such as a user asking for another account's documentsTest mandatory: one allowed and one denied request for each roleBlast radius rises to safety-or-legal and Detectability to never, because a leaked record raises no error
The failure branches store nothing and only show an error page, such as a 404 for a missing recordTest minimally: the happy-path test is enough; add a regression test when a branch breaksDetectability falls to same-day and Reversibility to trivial, because a user reports the wrong page and a redeploy fixes it
An error branch only passes the error up with Rust's ? operator, and the caller's handling has its own testsDo not test the branch that passes the error on; the compiler rejects code that drops itDetectability falls to immediately and Reversibility to trivial, because the build fails before the merge
A failure appears only when two services time out together under load, and reproducing it takes daysTest it differently: alert in production on the trace the failure leaves, such as duplicate rowsTest cost rises to heavy, while Detectability stays eventually and Reversibility with-effort

What breaks if you don't test

A test of the successful request never runs the catch block, the retry, or the branch for a missing record. When a rare branch is wrong, CI stays green until production takes that branch. A catch block that logs the error and carries on saves a signup without its team, or a retry creates the same booking twice. Customers meet the result days later.

What you lose if you over-test

Testing every error branch means a stub for every failure of every dependency: a service with three dependencies and four failure modes each needs twelve tests. Many assert log messages and retry counts, so they break when someone edits a log line that no user sees.

How to test

  1. Keep one test of the happy path, at the level where it is cheapest.
  2. List the failure branches and run the five factors on each. Test first the branches that write data or call another service.
  3. Force each failure with a stub of the dependency, and assert what the caller gets and what the database holds afterwards, with pytest's raises for the error.
  4. Leave out branches that store nothing and only show an error page; add a regression test when one breaks.

When the answer changes

  • A failure branch moves money or decides who may read a record.
  • The failure branches store nothing and show an error that users report the same day.
  • Forcing the failure takes days of setup across several services.

Real incident + Code example

The upload that saved a broken row

On a project management product I worked on, a document upload put the file in object storage, then saved a row for the file list. The only test covered a successful upload. When storage timed out, the catch block logged a warning and saved the row anyway. Storage timed out on about one upload in 2,000, and nobody noticed for five weeks, until a customer asked why 23 files in her project would not open. We found 212 broken rows, and support emailed their owners to upload the files again. The fix came with this test:

import pytest


class TimingOutStorage:
    def put(self, key, data):
        raise TimeoutError("storage timed out")


def test_upload_saves_no_row_when_storage_times_out(db):
    service = UploadService(storage=TimingOutStorage(), db=db)

    with pytest.raises(UploadFailed):
        service.upload(project_id=7, name="plan.pdf", data=b"%PDF")

    assert db.documents(project_id=7) == []

The stub raises the storage client's timeout error, so the test runs the branch the successful upload never reached.

FAQ

Should I only test the happy path?

Test only the happy path when the failure branches store nothing and show an error that a user reports the same day, such as a 404 page. When a failure branch writes data, calls another service, moves money or checks access, test that branch too.

Which unhappy paths should I test first?

Test first the failure branches that move money, check access, write data or call another service, because their mistakes often stay silent. Branches that only return an error page come last, since users report them and a redeploy fixes them.

Is it worth testing edge cases, or only the big picture?

Test the edge cases most likely to break as well as the whole flow, because a test of the whole flow passes whatever the error branches do. Choose edge cases by what their failure would damage, not by how many exist.

How do I test an error path when another service fails?

Replace the service client with a stub that raises the real client's error, such as a timeout, and assert what your code returns and stores. When forcing the failure takes days of setup, alert on its trace in production instead.