Delete all tests that contain imperative code
We are collectively creating the world that is best described as “Everything as code”. Not only do the apps and web sites we use on a daily…
We are collectively creating the world that is best described as “Everything as code”. Not only do the apps and web sites we use on a daily basis run on software code, many other things run on code. Cars, traffic and street lights, elevators, modern buildings, hospitals and ambulances, trains, airplanes, airports, train stations, you name it. Even our refrigerators, dishwashers, toasters, TVs, etc. run on code.
Code needs to be maintained
Unlike most other familiar products (e.g., chairs, desks, beds, doors, windows, etc.), software products are never done. When, for example, we start building a chair, we cannot offer it for use at the point where we’ve only attached two legs to it. We would have to wait for the final product — a chair with four legs attached, before we could offer it for sale.
Software is fundamentally different. In a software product, we can arrange for shipping end-to-end slices that could be potentially attractive to the paying customers. We don’t have to wait until the software product we are building is fully fleshed before it would make sense to offer it on the market.
So, not only is every software product susceptible to constant change as it evolves and grows in functionality, it is also susceptible to constant change due to the fast pace of evolution of the underlying technology.
Bottom line, unlike many physical products that, once built, require minimal (or no maintenance), software products require constant maintenance.
Tests are code
It is not possible to create a modern digital product without at the same time creating automated tests. Those tests serve the twofold function:
Tests inform and guide the design and the development of the shipping code
Tests provide safety net by alerting us whenever any change breaks the working digital product
Knowing that, we see that tests-as-code are an unavoidable maintenance burden for the software teams.
Tests need to be maintained
Same as with the shipping code, tests are also susceptible to change. As our digital product keeps evolving, the context and the underlying assumptions keep changing, and those incessant changes affect our tests as well.
However, while it is possible to view the shipping code as being an asset (our customers are paying for the shipping code), it would be hard to view tests as an asset.
Why? Because no customer would ever agree to pay for our tests. We cannot sell them. In that regard, tests are akin to scaffolding. When we’re building a high rise edifice, the crew needs to erect a scaffolding. When the building is finished, it gets placed on the market and people start buying apartments. But no person would ever be interested in buying the scaffolding.
Is scaffolding useful? You bet. Is it possible to sell it to people who would like to move into the building? No way!
Same applies to automated tests. Yes, they are super useful to the crew who is building and shipping the product. But that’s where the usefulness of tests stops.
Knowing that, we should strive to minimize the effort needed to maintain our tests.
How to maintain tests?
As we’ve mentioned, tests are code. The maintainability of the code gets to be a challenge for many teams. The more processing logic we include into our code, the harder and costlier it gets to maintain it.
It is for that reason that we must strive to avoid adding any processing logic in our tests. But how is that possible? Tests must also obey some processing logic, no?
Well yes, but that processing logic must not be implemented using imperative code. There are other ways to implement processing logic. Those ways make code maintenance much less costly, less risky.
OK Alex, you keep discussing this issue at a philosophical level, but we need a specific example. Can you guide us a bit?
Sure, why not. First, let’s make sure we understand what is meant by imperative code. To cut the long story short, most of the time we detect imperative code when it contains if-else/case-switch, and/or looping statements.
So, to simplify the discussion, it is paramount to avoid writing any imperative conditional logic in our tests. If we go back and examine the repos containing tests, we should get rid of any tests that contain if-else, or switch-case statements. Also, we must get rid of any tests that contain any loops. Those imperative constructs have absolutely no place in any test.
Every tests we write should only contain declarative code. What does that mean? The test must be simple, declaring the preconditions (i.e., declaring the expected values or the given state, which may also provide concrete input values), after which our test must exercise the shipping code (i.e., a trigger event, the when state). After that, the test must gather the values that the shipping code returns after running. And lastly, the test must assert by comparing the expected values with the actual values returned by the system after the shipping code ran and had transformed those values (the then state).
A test is an executable expectation, and that expectation must be declared. Keeping in mind that each test must have only one reason to fail, it is clear that there is no room in any test for looping or implementing branching/conditional logic via imperative code.
Takeaway: keep your tests small, focused, simple, and abolish any imperative logic in your tests.