Leverage use cases to identify different types of required tests
In software, we come across three common types of tests: unit tests, integration tests, and end-to-end (e2e) tests.
A top-down approach
While designing software, we start with an abstract description of the use case and decompose it into basic steps in terms of the feature of the underlying system. Since the feasibility and correctness of the entire use case hinge on each step’s feasibility and correctness, we can use the structure imposed by such decomposition of a use case to inform the different types of tests.
Use e2e test to test a use case, an integration test to test a step in a use case, and a unit test to test actions that enable a step.
Consider a successful checkout use case — the user uses her stored credit cards and addresses to successfully checkout the items in her cart. Suppose the following steps are involved in this use case.
- The user proceeds to checkout.
- The user selects one of addresses stored in her account.
- The user selects one of her stored credit cards as the payment method.
- The user chooses the shipping method.
- The user confirms the order.
Based on the above description, we can create an e2e test to exercise the system as a user would do (likely via a UI) to perform all of the above steps. This e2e test will test that every step in the use case can be executed successfully in the given order starting from a valid start state to arrive at the desired end state.
A step in a use case may involve multiple components and interactions between these components. To ensure the use case works correctly, we need to ensure that each step works correctly, and integration tests can help us achieve this. For example, in the above use case, the integration test for step 3 can exercise the code/logic associated with step 3 and the credit card managing component.
Finally, as we develop the code associated with each step in the above use case, we can test this code independent of external components such as credit card managing components (using test doubles).
- The above approach naturally associates unit tests to integration tests, integration tests to e2e tests, and e2e tests to use cases. This association can help ensure that we create the tests required to thoroughly test that a system supports the desired use case. Also, the association can help avoid redundant test executions, i.e., execute an integration test only if all of the associated unit tests pass.
- The above approach naturally ensures the set of tests associated with a system conforms to a pyramid structure, i.e., test pyramid. For example, in the above example, for happy paths, we will likely end up with one e2e tests, five integration tests, and more than five unit tests.
- While the above example focused on the happy path for the sake of brevity, please don’t forget to test sad paths.
- I suspect others may have figured out a similar top-down approach to identifying tests from designs. Recently, someone mentioned that the book Growing Object-Oriented Software, Guided by Tests suggests a similar top-down approach to derive tests. So, this book is on my reading list. If you know of other sources describing similar approaches, please share references :)