Skip to content

Instantly share code, notes, and snippets.

@MangaD
Created July 12, 2026 17:39
Show Gist options
  • Select an option

  • Save MangaD/569b1bcef4d1d7391630c405a0200bc3 to your computer and use it in GitHub Desktop.

Select an option

Save MangaD/569b1bcef4d1d7391630c405a0200bc3 to your computer and use it in GitHub Desktop.
Testing Software Beyond “It Works”: A Practical Guide to Failure-Oriented Testing

Testing Software Beyond “It Works”: A Practical Guide to Failure-Oriented Testing

CC0

Disclaimer: ChatGPT generated document.

Most developers instinctively write tests that prove their code works. That’s necessary — but it’s not sufficient.

Real-world software rarely fails because the “happy path” is broken. It fails because:

  • users do weird things
  • networks misbehave
  • disks fill up
  • APIs are misused
  • assumptions turn out false

So modern testing is not just about correctness. It’s about behavior under stress, misuse, and failure.

This article gives a conceptual map of the major testing philosophies that revolve around one core question:

How should software behave when reality doesn’t cooperate?


1. Positive Testing — Proving the Specification

Goal: Verify correct behavior for valid input.

You check the system does what it promises.

Example:

CHECK(parse_int("42") == 42);
CHECK(connect(valid_server));
CHECK(add(2, 3) == 5);

Positive tests answer:

Does the implementation match the specification?

They are essential — but dangerously comforting.

Passing positive tests only proves the demo works.


2. Negative Testing — Proving Correct Failure

Goal: Verify correct behavior for invalid input.

Instead of asking “does it work?”, you ask:

Does it fail properly?

Example:

CHECK_THROWS(parse_int("abc"));
CHECK_THROWS(connect("256.256.256.256"));
CHECK_THROWS(add_overflow(INT_MAX, 1));

Correct failure means:

  • no crash
  • no undefined behavior
  • no corrupted state
  • correct error reporting

Most production bugs live here — the error paths.


3. Boundary (Edge-Case) Testing — The Dangerous Valid Inputs

These inputs are technically valid… but risky.

parse_int("0");
parse_int("-2147483648");
parse_int("2147483647");
reserve(size_t(-1));

Edge cases sit between success and failure:

Type Input
Positive normal
Edge extreme but valid
Negative invalid

Many catastrophic failures come from boundaries, not invalid input.


4. Robustness Testing — Surviving a Hostile Environment

Now we stop blaming the user and start blaming reality.

We simulate conditions where the program still should behave safely:

  • timeouts
  • partial reads
  • packet loss
  • unavailable services
  • inconsistent state

Example:

disconnect_mid_transfer();
timeout_during_handshake();
half_open_connection();

The system may not succeed — but it must remain sane.


5. Fuzz Testing — Automated Chaos

Fuzzing feeds random data into your program:

""
"\0\0\0\0"
"AAAAAAAAAAAA"
random bytes
broken packets

Goal:

Discover failures you didn’t imagine.

Fuzzing is essentially infinite negative testing performed by a machine with no assumptions.

It finds:

  • crashes
  • memory corruption
  • UB
  • parser flaws

Many major security bugs are discovered this way.


6. Fault Injection — Breaking the Environment

Instead of invalid input, we simulate invalid reality.

We force system calls to fail:

  • allocation failure
  • disk full
  • DNS failure
  • clock jumps
  • packet loss

Example mindset:

What if malloc fails here?

If your code cannot survive that question, it’s fragile.


7. Chaos Testing — Production-Level Failure Simulation

Fault injection in development becomes chaos engineering in production.

You intentionally break the running system:

  • kill servers
  • add latency
  • drop requests
  • partition networks

Goal:

The system must degrade gracefully, not catastrophically.

Reliable systems are not those that never fail — they are those that fail predictably.


8. Security Testing — Adversarial Failure

Security testing is specialized failure testing.

The inputs are not just wrong — they are hostile.

Examples:

  • buffer overflow attempts
  • injection attacks
  • integer wraparound
  • path traversal

Here the tester behaves like an attacker.


9. The Underlying Philosophy

We can group all these approaches into two mental models:

Success-oriented thinking

“Does the feature work?”

Failure-oriented thinking

“What happens when assumptions collapse?”

Reliable software comes from the second mindset.

Users explore functionality. Reality explores failure modes.


10. A Practical Taxonomy

Category Question answered
Positive testing Does it work?
Negative testing Does it fail correctly?
Boundary testing Are limits handled?
Robustness testing Can it survive bad conditions?
Fuzz testing What failures did we miss?
Fault injection What if dependencies fail?
Chaos testing Can the system handle real outages?
Security testing Can an attacker break it?

11. The Engineering Lesson

Bad software breaks when something unexpected happens.

Good software reports an error.

Great software continues operating safely.

So testing maturity progresses like this:

  1. Prove correctness
  2. Prove safe failure
  3. Prove resilience
  4. Prove survivability

Final Thought

Testing is not about proving your code works.

It’s about proving the system remains trustworthy when the world stops behaving.

Because users don’t live in the happy path — they live in reality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment