moworks.dev
Back to blog

September 9, 2026

5 Types of Software Testing for More Reliable Systems

A practical guide to unit, integration, canary, fault injection, and performance tests, with lessons from almost a decade at Amazon.

Different tests catch different problems. Some check whether your logic works. Others check whether your services work together, whether production is still healthy, or what happens when a dependency fails.

After almost a decade at Amazon, designing and implementing these tests, here’s how I think about where each one adds value.

Unit tests

Unit tests check small pieces of code in isolation. They usually live alongside the application code and are particularly useful for conditional logic, regexes, data transformations, and conversions. They run during development and in pull request checks, before deployment.

What I absolutely despise is a blanket requirement for 100% unit test coverage.

I’ve worked in codebases where this produced mountains of tests for trivial getters and setters. Harmless refactors required updates to tests that mostly checked how the code was written, rather than whether it behaved correctly.

Coverage tells you which code ran during a test. It doesn’t tell you whether the test was useful.

Test meaningful behaviour and edge cases. Please don’t write tests just to satisfy a percentage.

Integration and end-to-end tests

Integration tests check whether parts of a system work together. An API test might call a running application and assert on its response, exercising the request handling, business logic, and database connection along the way.

These tests don’t require a deployed environment, but testing against one can also catch configuration, authentication, and connectivity problems that isolated tests miss. Which dependencies are real and which are simulated determines what you’re actually verifying.

This is where I often see a much better return on the amount of test code written. A single request can exercise a meaningful path through the application.

UI tests using tools like Selenium or Cypress can extend this to the user experience: open a page, click a button, and check the result. When they exercise the frontend and real backend services together, they’re end-to-end tests.

They’re slower and require more maintenance than unit tests, so focus on important journeys rather than every possible interaction.

Canary tests

By Canary tests, I mean recurring synthetic checks against a running system. Unlike deployment checks, these run on a schedule, whether or not anyone has shipped new code.

That distinction matters because deployments aren’t the only source of failures.

A memory leak might take hours to cause problems. Someone might change configuration outside your release pipeline. A dependency might change its API while its own tests continue passing, leaving your application broken.

A Canary exercises a meaningful operation repeatedly so you can detect those failures automatically. It won’t diagnose a memory leak for you, but it can reveal that requests have become slow or started failing.

Back these checks with alerts and sensible failure thresholds so someone can investigate and restore service.

Fault injection, or “Gremlin” tests

Fault injection tests deliberately break something to check how the system behaves and recovers. You might make a dependency unavailable, introduce latency, or force requests to fail.

Imagine a product page that depends on a pricing service. If that service goes down, you might want the page to keep working and display “Pricing currently unavailable.”

But does the implementation support that?

Can the API and UI handle a missing price? Do requests time out? Are retries bounded, or will they overwhelm the pricing service as it tries to recover?

Fault injection turns those questions into repeatable checks.

Small, controlled scenarios can run in CI/CD. Broader chaos experiments may run separately, with safeguards to limit their impact. Either way, recovery should be something you’ve tested.

Performance and throughput tests

Performance tests check how a system behaves under load. How many requests can it handle while keeping latency and errors within acceptable limits?

The workload needs to resemble actual usage. That means a representative mix of reads and writes, small and large requests, and common and expensive operations. Testing one cheap endpoint repeatedly won’t tell you much about the whole application.

Increase the load and measure completed requests, failures, and latency, including slower responses at the tail. A throughput target is meaningless if you reach it by returning errors or making users wait.

For repeatable comparisons between releases, a fixed environment, sometimes a single instance, helps isolate changes in application performance. Separate tests should check how the full system scales.

These tests can run at a dedicated stage in CI/CD, with explicit thresholds for throughput, error rate, and latency.

Put them together

Each layer answers a different question: does the logic work, do the components work together, is the system still healthy, can it survive failures, and can it handle the load?

Robust systems need answers to all five. The balance depends on the product, but chasing unit test coverage alone won’t get you there.

I spent almost a decade at Amazon designing and implementing these kinds of tests. If you’re looking for someone to help build reliable software and a release process you can trust, I’m happy to help. Get in touch.

I wrote the ideas and the substance of this article. AI helped with proofreading and formatting. The technical details were accurate at publication.