In many circumstances, it can require considerable thought and logic in automated tests to identify and retrieve the ‘actual values’ for a test so that we can compare them with ‘expected values’.
Designing tests at the same time as the actual feature code, instead of afterwards enables teams to identify where it is useful to embed and lever that logic in the actual product itself instead of leaving it buried in test code. Logic that would remain buried in test code becomes a valuable feature instead and improves the testability of product.
For example, take a test that needs to compare the contents of a complex data structure with expected values provided in a simpler. We could bury code that converts the complex data into the simpler format inside the test. Or we could make that conversion routine part of the product providing callers with a means of extracting that data in that simpler format.
In a dev-ops environment, code to examine the results of some deployment operation could be buried inside test code. Alternatively, the feature itself could use that same logic to tell if it has completed successfully or not, providing more detailed feedback to the caller. Test code simply reuses that part of the feature to do all or part of its result checking work.
In addition, if a test performs a sequence of steps that commonly need to be performed one-by-one by a user or caller, we could consider automating those steps to make a useful feature. For example, if a customer always has to perform a set of initial steps in a particular scenario, a good end-to-end smoke test is likely to walk through those steps to ensure the scenario works properly. To be more flexible and resilient, we could make this a data-driven test, maybe by giving it a JSON object that provides the input data for that initial steps. If at planning, however, we know we have to do the work to code this test, then we can consider making most of this code available as a service. Then a customer can use that service to perform those steps automatically instead of having to do them manually, one-by-one, waiting for each one to complete before performing the next step