Time is an integral part of our lives — the same for the applications we develop. From the software engineering perspective, time is just another dependency which we introduce to our systems. And that's OK. The situation might start to be problematic when some parts of the system are more dependent on time. How to test this component? How to mock the time?
In this article, I'm going to show you our approach to develop and test components which depend on time. We'll get rid of time-dependency by creating a proper abstraction and applying the dependency inversion principle. Ultimately, we'll create a mechanism to mock time in tests. Ready?
Time is continuous — its value is changing all the time. Basically, it's impossible to reach the same application state, when it depends on time. Let's consider this example:
This code reflects business rule — the order should get obsolete if is no updated for 30 days. To perform this filtering, the service needs to have knowledge about the current date and time.
I told that time is nothing more than another dependency. But how we can deal with it? Is time something that we can e.g. inject into our services? Yes, if we use a proper abstraction.
If you look at the clock, you get to know what time is it. Perfect! Let's define a simple interface for the clock.
Do you need more? Feel free to extend this interface. For our case, it’s enough. Let me propose a simple implementation.
Yes, I did it literally, but it might be even better. The implementation detail — Clock — is clearly visible now, but the code breaks Dependency Inversion Principle. We need to fix it.
Remove low-level dependency
According to Robert C. Martin’s definition of the Dependency Inversion Principle:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
Mocking time in functional tests
This is an example test scenario from our system, which checks the correctness of the invoicing process. It's written as CodeceptionCest scenario (simplified version)
Under the hood, this process uses the current time to perform necessary filters and checks. Unfortunately, on different days we've received different outcomes. To make the whole process replayable, we have to run it with the same conditions. What about time?
We've added a custom Codeception helper, which adds extra method. We put it in necessary scenarios before the actual test case.
The Codeception helper for Yii2 Framework may look as follows:
As you can see, by hiding time dependency behind the dedicated abstraction, we get more flexibility when it comes to tests. Our code is cleaner and easier to maintain.
We had been looking for a solution which would have the smallest impact on our existing codebase. Some tools were well-known for us, e.g. libraries like Carbon. We found also a different and richer implementation of the clock in PHP. However, we didn't want to add another library to our project to cover this specific problem.
Aspect-oriented Programming (AOP) offers a perfect solution for this problem defining special hooks before execution of specific function or method. It's a powerful tool, which doesn't require any modification of existing code. Moreover, it covers all third-party code as well. Unfortunately, it requires introducing aspects – another, sometimes magical way of defining extra behavior, what can be hard at the beginning.
Ultimately, we decided to implement the simplest solution based on Dependency Injection. This technique is well-known in OOP and it shows intentions explicitly, what is definitely plus.