Instead of stubbing dates, use a parameter
November 5, 2020
I work with dates. A lot of dates. And when you work with dates a lot, it’s real tempting to write code that references whatever Date
object you want to construct directly, like this:
# Contrived example
def current_check_dates_for_payroll(payroll)
Payroll.where(check_date: Date.today)
end
I actually did a lot of things like this when I was first starting out at Gusto. And what you find is that once you go down this route, you start to include wonky dependencies that allow you to “stub out” what day it is, so you can unit test your stuff correctly.
I don’t think this is explicitly wrong since there are so many dedicated libraries that are meant to do this for you (there’s clearly a need for them). But I’ve evolved to try to include dates as a parameter instead, like this:
def effective_dates_for_payroll(payroll, current_date: Date.today)
Payroll.where(check_date: current_date)
end
Why do I think this is better?
- We remove any package dependency on date stubbing libraries when writing tests. All tests can be written without having to provide any fancy packages. Also: less likely to pollute other tests with incorrect dates this way.
- We invert the control and allow the caller to use whatever date they want (but the default is still the current date, if none are provided). This allows us to generalize this helper and use it whenever we’re in a pinch.
- As a concept, this is very easily applied to other languages. I’ve done this exact thing many times for front-end code when using TypeScript, and in React components (as a prop instead of a parameter).
A real life scenario
Back in my first year as a real software engineer, I added a check for a specific date inside of a particular Sidekiq job for a feature I was working on. The day after we launched the feature, that Sidekiq job failed, and needed to be manually triggered by one of us.
However, a more senior engineer looked at the code with the embedded date, and said we couldn’t manually trigger the job since the date check was essentially hardcoded into the function definition. We had to wait for a deploy with the inversion of control applied to be able to manually kick off the job.
While I was lucky that the impact of the job failing didn’t touch our core payroll product, what if it did? There is a tight timeline when it comes to submitting payments that need to be processed in the next day. Waiting any amount of time or using brainpower to figure out a workaround for an issue like this can be very expensive1.
Overall — I don’t think I will be too bold in saying that this is generally the right thing to do if you want the most flexibility for your code. I think a bigger argument can be made for stubbing dates the more “stuff” your test tries to do (think Capybara or Cypress tests), but that kind of discussion might be beyond the scope of this post.
- Gusto’s codebase is still primarily monolithic, and back in the day it took an absurd amount of time to build and deploy productionized code. But in the last 1-2 years, we’ve really nailed down our application infrastructure for our main codebase and it is loads faster. My colleague wrote a really good blog post on how we did it.