When designing a software application/system, it is very important to avoid design decisions that will depend on hidden inputs. What do I mean by “hidden inputs”? Let me give an example: say we want to automate some business operation where the decision regarding how much to discount a product or a service depends on the time of day (perhaps a bit contrived example, but bear with me). The easiest way to code that solution is to leverage the infrastructure commodity called “the system clock” and fetch the current time from that underlying commodity. Then, based on the fetched value, our processing logic will make the decision that is in compliance with the stated business policy rule.
We write a test (an executable expectation) where we specify that if the time of the day is before noon, the discount is different than if the current time is later than the noon time.
We run the test, and it fails (because we haven’t implemented that processing logic yet). We then write some code — a module that fetches the current time from the system clock and based on the value decides which discount to apply.
Can you spot a problem with this? I hope you can. But just in case you can’t, let me elaborate: the test we wrote specifies certain discount value that gets applied if the clock says that the current time is earlier than noon time. We now run the test again, and this time it passes. Great!
Let’s say we keep working, and then come afternoon, we run the test again. But this time the test fails! Why? Well, the current time is now the afternoon time, so the routine accordingly calculates different value for the discount. Naturally, the different values causes the test to complain when asserting the expected vs the actual value.
The current design makes our system untestable! Which is a really bad thing.
How to fix the untestable system?
The solution is simple — it’s called Separation of Concerns, and is implemented in the Single Responsibility Principle (SRP). Currently, our module is mixing two concerns — on one hand, it is concerned with obtaining the current time, while on the other hand it is also concerned with calculating the correct discount based on the current time.
That’s a bad design. Those two concerns must be separated. Once we do that, we will end up with a module that is responsible for only one thing — calculating the correct discount based on the current time.
How does that module know what is the current time? Simple — the system passes the correct time as a value (usually that passing of the value is called sending a parameter, or an argument).
We now have to modify our original test. Now the test has the responsibility to fabricate the time specified by the business policy rule.
How will the test accomplish that? Will we make the test code call the system clock? What do you think?
No, we shouldn’t do that. It would only replicate the original problem (this time reshuffling it from the shipping code to the code written in the test).
Instead of repeating the same mistake, the test should simply provide a premeditated value. Say for instance, the test may declare that current time is 9:00 o’clock. Or any other time earlier than the noon time (12:00 o’clock).
Now the test will just send that value into the module under test, and the test will always pass, regardless of the environmental conditions.
Why is database a hidden input?
Database is an instance of an auxiliary storage. As such, it must be accessed by the application across the so called I/O boundary (Input/Output boundary). Because database is outside of the application that we’re building, it becomes a globally shared resource. Other applications can also access that same database. Because of that, the database becomes a storage of the globally shared mutable state.
It is customary that software applications depend heavily on accessing databases. That habit is potentially very worrisome, because as soon as we have globally shared mutable state, we open our processing logic to all kinds of race conditions. And race conditions are never good, because such conditions can potentially corrupt the validity of the stored data. Which is a situation we often call a bug, or a defect.
OK, cool, got that, but what about this hidden input issue? Well, same as with the system clock that we described in the above analysis, database is a commodity layer containing information which is liable to change. If we create a test that specifies certain expectations based on the values stored in the database, those values are liable to change. When that happens, tests that used to pass will suddenly start failing.
Same as with the system clock, instead of designing our system to rely on the hidden input from the database, we should separate the concerns in our code and craft a module that implements a Single Responsibility Principle (SRP). That module will only be concerned with processing the received values. It will not care whence those values came from — maybe those values came from a database, maybe they came from some resource on the network, maybe they came from a file in the file system, don’t matter. The important thing is that the values are here, and those values must now be processed.
When we design our system like that, we will deliver a deterministic system that is immune to environmental conditions. System configuration may change, still our code that is responsible for processing business policy rules is blissfully ignorant of those changes. That apathy for what’s going on outside of the business domain makes our system very robust, flexible, and resilient.