Choosing what to test
Before writing any test, you need to decide what the test should verify. Three criteria can guide this decision:
- Cover the happy path. This is the primary user story of your application. Any error here will be noticed immediately by your users.
- Choose your level of detail deliberately. Invest more granular testing where the use case is vulnerable or where an error would cause significant damage.
- Prefer lower-level tests. Prioritize unit and integration tests over end-to-end tests whenever possible, as they are faster and easier to maintain.
Test case fundamentals
A test case defines a sequence of actions and expected outcomes used to confirm that software behaves as intended. When a test runs, it performs two types of checks:
- Verification. Checks that the software functions without errors and meets non-functional requirements. The question addressed is: "Are we building the product right?"
- Validation. Checks that the software aligns with the stakeholder's high-level requirements. The question addressed is: "Are we building the right product for the user's requirements?"
If the software fails to produce the expected result, the test reports an unsuccessful outcome and signals that the issue needs investigation. Input data or conditions grouped for checking are called equivalence classes; these groups should elicit similar behavior from the system under test.
Common test paths
Most test cases fall into one of three paths:
The happy path—also called the "golden path"—covers the most common use case of your feature. This is what users expect to work under normal conditions.
The negative test, sometimes called the "scary path," is frequently skipped because it covers uncomfortable scenarios. It deliberately targets conditions that cause the code to misbehave or enter an error state. These tests are crucial for highly vulnerable use cases that impose substantial risk on users.
Several other, less commonly used paths exist. The table below summarizes them:
| Test path | Explanation |
|---|---|
| Angry path | This leads to an error, but an expected one; for example, if you want to ensure error handling works correctly. |
| Delinquent path | This path takes care of any security-related scenarios your application needs to fulfill. |
| Desolate path | The path testing the scenario in your application doesn’t get enough data to function, for example, testing null values. |
| Forgetful path | Testing the behavior of your application with insufficient resources, for example, triggering a data loss. |
| Indecisive path | Testing with a user who is trying to do actions or following user stories in your application but hasn’t completed those workflows. This could be the case, for example, when the user interrupts their workflow. |
| Greedy path | Trying to test using vast amounts of inputs or data. |
| Stressful path | Trying to put a load on your application by any means necessary until it no longer functions (similar to a load test). |
Two established methods help categorize test inputs and reduce redundancy:
- Equivalence partitioning. This groups test cases so that a single representative input validates the whole group. Because inputs within the same equivalence class should behave identically, this approach reduces the total number of test cases needed.
- Limit analysis. Also known as boundary-value analysis, this examines the extremes of input ranges to uncover defects that appear only at the limits of the system's capabilities.
Structuring written test cases
A classic, manually documented test case contains structured data captured in a table. Two patterns help organize these test cases consistently:
- Arrange, act, assert. Known as the "AAA" pattern, this structures a test into three stages: setting up the conditions, performing the action, and drawing conclusions from the result.
- Given, when, then. This follows the same structure but originates in behavior-driven development.
Here is a classic example of a documented test case following these patterns:
| Information | Explanation |
|---|---|
| Prerequisites | Everything which needs to be done before writing the test. |
| Object under test | What needs to be verified? |
| Input data | Variables and their values. |
| Steps to be executed | All the things you will do to bring your test to life: all actions or interactions you do in your tests. |
| Expected result | What should happen and which expectations are to be fulfilled. |
| Actual result | What actually happens. |
For automated tests, such exhaustive documentation is helpful but not required. The information is present in the test itself. The same test case, expressed as an automated test, looks like this:
| Information | Translation into test automation |
|---|---|
| Prerequisites | All the things you need, arranging the test, and thinking about what is given to make your test’s scenario happen. |
| Object under test | This “object” can be various things: an application, flow, unit, or component under test. |
| Input data | Parameter values. |
| Steps to be executed | All the actions and commands executed inside your test, the things you act upon, and finding out what happens when you do certain things. |
| Expected result | The assertion you use to validate your application, the things you assert upon in your application. |
| Actual result | The result of your automated test. |



