What Could Go Wrong? A Developer’s Guide to Better Testing
Testing Software from Methods to Complete Systems A method returns the correct value. An API responds with 200 OK. A page renders successfully. These are useful checks. But what happens when the backend is unavailable, a user closes the application during an operation, or a hardware command succeeds too late? When I think about software testing, I think about several layers of confidence: individual logic, components, interactions, and the complete system. At every layer, I ask two questions: Does it do what the requirements say? What happens when something goes wrong? 1. Start with unit testing while writing code Unit tests check a small piece of behavior in isolation, such as a function, method, or class. This is done to verify the behavior itself and not make an exhaustive test for each method. In case of backend business logic, we can use: Valid inputs and calculations based on them. Missing inputs or null/invalid inputs. Boundary value, i.e., values like ze
Testing Software from Methods to Complete Systems
A method returns the correct value.
An API responds with 200 OK.
A page renders successfully.
These are useful checks. But what happens when the backend is unavailable, a user closes the application during an operation, or a hardware command succeeds too late?
When I think about software testing, I think about several layers of confidence: individual logic, components, interactions, and the complete system. At every layer, I ask two questions:
- Does it do what the requirements say?
- What happens when something goes wrong?
1. Start with unit testing while writing code
Unit tests check a small piece of behavior in isolation, such as a function, method, or class. This is done to verify the behavior itself and not make an exhaustive test for each method.
In case of backend business logic, we can use:
- Valid inputs and calculations based on them.
- Missing inputs or null/invalid inputs.
- Boundary value, i.e., values like zero, maximum, and values close to them.
- Exceptions and behavior in case of dependencies failure.
The validation method should support values up to the permissible limit and fail the values slightly exceeding the limit. Testing only a typical value leaves the boundary unverified.
Mocking/stubbing helps in isolating dependencies, where the test for service doesn't have to establish a connection with actual database and can mimic the return value or exceptions thrown by a repository.
Similarly, UI logic can be tested in isolation. Based on the data passed to the UI, Does the UI show the correct label or disable the button? Include the testing of empty/loading data screen too.
However, one thing must be clear – even when using mocks, you do not have to call a test a unit test. An API test that involves loading routing, serialization and framework behavior might actually be a component test or an integration test, even when downstream services are being mocked.
2. Test each component or subsystem
Component testing verifies a significant portion of the application as a whole. This can be a user interface component, an API service, or an alarm management system, with external dependencies managed where necessary.
Different terminology is used by different teams; some use “component” and “unit” interchangeably. In this context, component testing is used for the boundary larger than a single function or class.
Take the screen that initiates the operation and presents the status of it. The component tests can check that:
- The starting state is correct.
- Invalid input stops submission.
- The loading state is shown while processing is pending.
- There is an informative message in case of an error.
- Controls return to the appropriate state after an error.
- A stale response must not overwrite the state of a newer request when the latest request should take precedence.
For the backend component, validate test scenarios, business logic, error handling, and cleanup in case of exceptions.
This level provides answers to the following targeted question: Does this subsystem work properly within its own boundaries?
3. Test the interactions through integration testing
Individually correct components can still disagree about request formats, response fields, authentication, or timing.
Integration tests verify that connected components work together as expected. This could be a service talking to a database, a UI interacting with a backend, or services calling other services.
Selenium can test browser workflows. Postman and curl can test the API calls. Manual tests could help to investigate connections that require human judgement. The scope of the test determines its level, regardless of the tool used.
Questions to ask at each boundary:
- Does the receiver parse the request correctly?
- Do the response fields and error formats fit together?
- Does the authentication/authorization work?
- What happens if the response is slow or does not come back?
- Could the duplicate requests lead to the duplicate actions?
Mock which always gives the expected response will not help you to prove that the real dependency really does so.
4. Test the complete system in its operating context
System testing validates the behavior of the built system against its requirements. In case of software that works with hardware, this involves physical behavior and timing.
Consider a scenario when the alarm triggers a light to come on. The backend unit test could confirm that the application calls the function which would make the light go on. This does not mean that it actually does happen in the required timeframe.
The system-level questions are:
- Is the necessary light activated upon alarm?
- Is the timing within the required bounds?
- Is the UI reflecting the right system state?
- Is the user able to cancel the process via appropriate command?
- How does the system behave in case the device gets disconnected mid-way?
- Is the right safe state achieved after failure?
Simulate those scenarios which you can do accurately enough using simulation. Test any other relevant behavior via hardware where necessary. Successful command from simulation does not imply physical action.
The above approach should be used even in case of pure software systems.
5. Treat failure behavior as part of the feature
Testing should cover the API's documented response contract, rather than stopping at a successful response.
| Scenario | What to verify |
|---|---|
| 400 Bad Request | Invalid input receives an actionable explanation. |
| 401 Unauthorized | Missing or invalid authentication follows the defined sign-in or session-recovery flow. |
| 403 Forbidden | Access is refused, and restricted operations remain protected. |
| 404 Not Found | An unavailable resource is handled without leaving the UI stuck. |
| 409 Conflict | Conflicting state is explained, with an appropriate recovery path. |
| 500 Internal Server Error | The failure is handled without exposing internal details. |
| 503 Service Unavailable | Temporary unavailability produces the defined fallback or recovery behavior. |
| Timeout or connection failure | The application exits the waiting state and explains what the user can do next. |
The status codes describe HTTP outcomes; the application-specific responses in this table are behaviors to define and test. A connection failure may produce no HTTP response at all.
Do not retry every failed request blindly. If an operation might already have completed, retrying can create duplicate work. Test the intended retry policy, including limits and protection against duplicate effects.
6. Check lifecycle, browser, and screen behavior
Unexpected application closure deserves deliberate testing.
If a user closes the UI during an operation, should the backend continue, cancel, or complete a controlled shutdown? There is no universal answer. The requirements and architecture must define it.
A browser tab closing does not automatically stop a backend process. Browser close warnings also have limitations, so important cleanup should not depend entirely on a warning appearing.
Test refreshes, reconnects, session expiration, and reopening the application. Can the user recover the actual operation state?
For supported browsers and devices, check more than appearance:
- Are important controls visible at smaller widths?
- Do dialogs remain usable on laptops, tablets, and phones?
- Can users navigate with a keyboard and identify focus?
- Are errors understandable without relying only on color?
- Does resizing preserve data and application state?
Compatibility and accessibility are concerns across testing levels. A component test can check a dialog, while a system test checks whether the complete workflow remains usable.
7. Verify requirements, not just existing behavior
A test can accurately describe the current implementation and still approve the wrong behavior.
Write formal verification cases from approved requirements and acceptance criteria. Use observed behavior to investigate the implementation, not as the sole authority for the expected result.
For an illustrative alarm requirement, a verification case might look like this:
| Field | Example |
|---|---|
| Requirement | The indicator activates within the specified interval after the alarm event. |
| Preconditions | System ready; indicator connected; timing measurement available. |
| Action | Trigger the defined alarm condition. |
| Expected result | The correct indicator activates within the requirement's timing limit. |
| Evidence | Recorded event and activation times, plus relevant logs. |
| Verdict | Pass or fail based on the specified limit. |
Do not invent a timing limit because it seems reasonable. If a requirement says “immediately” without a measurable definition, clarify it before using it as a pass criterion.
During execution, record the actual result for each verification step. An unmet expected result is a failure. If the test cannot proceed, record it as blocked or not executed according to the team's process; do not mark it passed.
When a defect is found:
- Create a defect report with reproduction steps, expected and actual results, environment details, and evidence.
- Link it to the affected requirement and test case.
- Fix the cause and add an appropriate automated regression test where practical.
- Rerun the failed case on the corrected build.
- Run relevant regression tests to check for effects elsewhere.
Passing a rerun is evidence about the corrected build. Keep the original failure and its investigation traceable.
Make testing a way of thinking
These levels are complementary, and their execution need not follow a rigid sequence. Fast isolated tests support development; broader tests provide evidence about interactions and complete behavior.
Choose the smallest test that can reveal a particular defect, then add broader coverage where boundaries, operating conditions, or risk require it.
Before calling a feature complete, ask:
What could fail, what would the user experience, and what evidence proves the system responds as required?
That question connects method-level correctness to the reliability of the complete system.
References
Selenium: Types of Testing
Selenium: Overview of Test Automation
Google web.dev: Types of Automated Testing
RFC 9110: HTTP Semantics