Testing That Works

A practical guide to choosing the right tests, avoiding brittle suites, and shipping software with confidence.

4 min read

Good testing is not about writing as many tests as possible. It is about building a safety net that catches real problems early, explains failures clearly, and gives teams confidence to change software without fear.

Two of the most useful testing layers are unit tests and integration tests. They solve different problems, and healthy projects usually need both.

What unit tests are for

Unit tests check a small piece of code in isolation: a function, method, class, component, or module. The goal is to verify the behaviour of one unit without relying on databases, networks, file systems, queues, or other external services.

Because unit tests are narrow, they should be fast, deterministic, and easy to run during development. A developer should be able to run them repeatedly while editing code and get feedback in seconds.

Unit tests are especially useful for:

  • Business rules and calculations
  • Validation logic
  • Error handling paths
  • Data transformation functions
  • Edge cases that are hard to reproduce manually

For example, if an order total should include discounts, tax, and shipping rules, a unit test can cover those combinations without starting the whole application. When the test fails, the cause is usually close to the code being tested.

What integration tests are for

Integration tests check whether multiple parts of a system work together correctly. Instead of isolating one function, they exercise real connections between modules, services, databases, APIs, message brokers, or third-party systems.

Integration tests answer questions that unit tests cannot:

  • Can the application read and write to the database correctly?
  • Do API routes call the right services and return the expected responses?
  • Does authentication work across the web layer, session store, and user database?
  • Do background jobs process real messages in the expected format?
  • Do configuration, migrations, and infrastructure assumptions match reality?

These tests are usually slower than unit tests, but they catch problems that only appear when pieces are connected. A perfect set of unit tests can still miss a broken SQL query, a missing environment variable, a mismatched API contract, or an incorrect dependency configuration.

The key difference

The simplest distinction is this: unit tests protect logic, while integration tests protect connections.

If a function returns the wrong result for a specific input, a unit test should catch it. If the application cannot save a valid record because the schema, ORM model, and service code disagree, an integration test should catch it.

A useful testing pyramid

Many teams use the testing pyramid as a rule of thumb: lots of unit tests, fewer integration tests, and a smaller number of end-to-end tests. This shape exists because lower-level tests are faster, cheaper, and easier to debug.

That does not mean integration tests are less important. It means they should be chosen carefully. A small number of well-designed integration tests can provide more value than hundreds of broad, slow, fragile tests.

Common mistakes

One common mistake is mocking too much. If every dependency is replaced with a mock, tests may only prove that the code works with the developer's assumptions, not with the real system. Mocks are useful, but they should not hide important contracts.

Another mistake is making integration tests too broad. If one test starts the full application, touches the database, calls several services, and checks many outcomes, a failure can be difficult to diagnose. Integration tests should still have a clear purpose.

A third mistake is testing implementation details instead of behaviour. Tests should describe what the software must do, not lock the internal structure so tightly that harmless refactoring breaks the suite.

Practical guidelines

  • Use unit tests for pure logic, branching rules, and edge cases.
  • Use integration tests for database access, API boundaries, authentication, queues, file handling, and service contracts.
  • Keep unit tests fast enough to run constantly during development.
  • Keep integration tests focused and representative of real production behaviour.
  • Avoid excessive mocking around the parts of the system where bugs are most likely to occur.
  • Make test names readable so failures explain the intended behaviour.
  • Run the fastest tests locally and the broader suite in continuous integration.

What good balance looks like

A balanced test suite gives fast feedback while still checking real system behaviour. Unit tests should tell you whether the logic is correct. Integration tests should tell you whether the important pieces still work together.

When both layers are used well, testing becomes less of a chore and more of an engineering advantage. Developers can refactor safely, reviewers can trust changes more easily, and releases become less dependent on manual checking.

The best test suite is not the biggest one. It is the one that catches meaningful regressions, runs reliably, and gives the team confidence to keep improving the product.