Test Driven Software Development

How test-driven development turns use cases into repeatable checks, clarifies unit, integration, system, and acceptance tests, and gives teams confidence to change and release software.

Testing is how a team proves that software still does what it promised to do. Without tests, every release pushes that responsibility onto the user.

Test-driven development makes that proof part of the design process. It asks the team to describe the expected behavior before writing the code that implements it. That discipline is useful because it forces vague requirements to become observable checks.

Why This Matters

Software gets harder to change when nobody trusts it. A small edit feels risky because the team cannot tell whether it broke something in another part of the system. Releases slow down, bugs reach users, and developers start protecting fragile code instead of improving it.

A good test suite changes that dynamic. It gives developers a fast signal while they work, reviewers a concrete way to check behavior, and the release process a repeatable gate before software reaches users.

The goal is not tests for their own sake. The goal is confidence: confidence that a requirement is understood, that a change behaves as intended, and that existing behavior still works.

The TDD Loop

The classic TDD loop is short:

  1. Write a failing test for the behavior you want.
  2. Write the smallest code that makes the test pass.
  3. Refactor the code while keeping the test passing.

That first step is the important one. A failing test proves that the behavior is not already present and that the test can detect the missing behavior. If the test passes before the implementation exists, the test is not proving what you think it is proving.

TDD is difficult less because of syntax and more because it requires decision making up front. You have to define the behavior clearly enough to test it.

From Use Cases to Tests

Tests should connect back to user value. The path usually looks like this:

ArtifactQuestion it answers
Use caseWhat does the user need to accomplish?
RequirementWhat behavior must the system provide?
Acceptance testHow will we know the behavior works?
TaskWhat implementation step should a developer complete?
Unit/integration testsWhat lower-level checks protect the implementation?

For example, the garden use case might say:

As a gardener, I want the system to water a zone when the soil is too dry and stop when the soil reaches the target moisture range.

That should become acceptance tests before it becomes a pile of tasks. The acceptance tests define the observable behavior:

GivenWhenThen
Soil moisture is above the dry thresholdA reading is processedThe pump remains off
Soil moisture falls below the dry thresholdA reading is processedThe pump turns on
The pump is onSoil moisture reaches the damp thresholdThe pump turns off

Those tests create a contract for the implementation. Unit and integration tests then support that contract at smaller boundaries.

See Use Cases and Use Cases to Tasks for the planning side of this workflow.

Types of Tests

Different tests answer different questions. A healthy test strategy does not treat every check as the same kind of thing.

Unit Tests

Unit tests check a small piece of behavior in isolation: a function, method, parser, validator, calculation, or policy object.

They should be fast, focused, and easy to run while developing. A unit test should usually make it obvious what broke when it fails.

Good unit test targets include:

  • Threshold calculations.
  • Input validation.
  • Topic parsing.
  • Configuration defaults.
  • State transitions.
  • Error handling branches.

Integration Tests

Integration tests check that two or more components work together. They are useful where bugs often appear at boundaries: database access, HTTP handlers, MQTT clients, file formats, queues, and external APIs.

An integration test might verify that an MQTT message is converted into a gateway reading or that a REST handler returns the expected JSON shape from a cache.

These tests are usually slower than unit tests, but they catch failures unit tests cannot see.

System Tests

System tests exercise the application from the outside. They treat the software as a running system and interact through public interfaces.

For an IoT gateway, a system test might publish a fake sensor reading with mosquitto_pub, call the REST API with curl, and verify that the reading appears in the response.

System tests are valuable because they check real wiring: processes, configuration, protocols, and runtime behavior.

Acceptance Tests

Acceptance tests define whether the work satisfies the user-facing requirement. They are written from the outside, in language that a user, product owner, reviewer, and developer can all understand.

Acceptance tests are the bridge between use cases and release readiness. Before a feature ships, its acceptance tests should pass without special manual interpretation.

Automated and Manual Tests

Automated tests should do as much of the repeatable work as possible. They are fast, consistent, and can run on every change.

Manual tests still have a place when the behavior involves physical systems, unusual hardware, visual judgment, or safety checks. The key is to make manual tests explicit instead of relying on memory.

A useful rule is:

  • Automate checks that are deterministic and repeated often.
  • Document manual checks that cannot yet be automated.
  • Keep reducing the manual portion as the system matures.

What a Good Test Suite Provides

A good test suite should be:

  • Repeatable: the same input produces the same result.
  • Fast enough: developers can run the relevant tests frequently.
  • Selective: one package, component, or scenario can be run alone.
  • Layered: unit, integration, system, and acceptance tests each have a clear purpose.
  • Visible: failures produce actionable output in local development and CI.
  • Connected to requirements: acceptance tests trace back to use cases and release criteria.

Tests should make change easier. If tests are slow, brittle, vague, or unrelated to real behavior, they become another maintenance burden.

Common Pitfalls

Testing Implementation Instead of Behavior

Tests that know too much about private implementation details fail every time the code is refactored. Prefer testing observable behavior at the right boundary.

Writing Tests After the Code and Calling It TDD

Tests written afterward can still be valuable, but they do not provide the same design pressure. TDD means the test shapes the implementation.

Mocking Everything

Mocks are useful, but excessive mocking can create tests that only prove that mocks were called. Use mocks at true external boundaries and keep important behavior covered by real unit or integration tests.

No Acceptance Tests

A project can have many unit tests and still fail the user. Acceptance tests keep the team honest about the actual promise made by the use case.

Tests That Are Hard to Run

If tests require hidden setup, special machines, or tribal knowledge, developers will run them less often. Make the common path boring: make test, go test ./..., or the closest equivalent for the project.

TDD and Release Confidence

Testing is part of the release process, not a separate ritual. A release pipeline should run the checks that prove the build is ready: unit tests, integration tests, static checks, and acceptance tests appropriate to the system.

When those checks are reliable, releases become less dramatic. The team can ship smaller changes more often because each change passes through the same evidence-producing path.

See Release Process for how tests fit into versioning, CI/CD, deployment, and rollback planning.

Practical Checklist

Before implementing a feature, ask:

  • What use case does this support?
  • What observable behavior proves the requirement works?
  • What acceptance test should pass before this ships?
  • Which unit tests protect the core logic?
  • Which integration tests protect the important boundaries?
  • Which system test proves the pieces work together?
  • Where will these tests run in CI before release?

A good answer to those questions turns testing from an afterthought into an engineering control system.

Related reading

  • Organizing Software Projects with Kanban How Kanban boards make work visible, keep tasks small, and give developers, managers, and customers a shared view of project progress.
  • Software is Hard Why software projects become unpredictable when user expectations and developer assumptions diverge, and how lightweight engineering …
  • Version Control Systems Git, branching strategies, tags, pull requests, and commit discipline: how a version control system becomes the source of truth for a …
  • Peer Reviews How peer review catches bugs before users do, spreads knowledge across the team, and keeps code maintainable through clear author and …
  • Release Process How to ship software with versioning, CI/CD, release artifacts, changelogs, smoke tests, rollback planning, and a repeatable release …