As a testing advocate, I frequently delve into “implementation details” and “public interfaces.” These terms refer to the inner workings of your API and how it’s presented to users. Rather than just defining them, let’s explore their significance through a real-world example. Witness how grasping these concepts enhances our code in practical scenarios.
Table of Contents
Once Upon a Date
Our story begins with a date. Not just any date, though – a date picker.
While the date picker seems straightforward – show an input and a dialog with a calendar – a lot is going on behind the scenes.
For instance, in order to generate the calendar, one needs to generate the grid correctly. Notice that while we have five weeks in the grid, the days of week are not always the same day of the month. In addition, the “padding” of the first and last weeks with days of last and next month is also not constant.
The first step would be to generate the data set we will render according to. This will be the part we will focus on in this article.
Let’s see how it was implemented.
Testing and Creating the Grid
We want to create the utility function buildCalendarGrid
. As its name suggests, it’s going to build the grid for us. The implementation is not that important, but for reference, I’m going to share an image of the tests so you can see they are quite comprehensive. The code is folded, so there is no need to read:
Let’s take a quick look at the implementation that makes the tests pass. Again, there’s no need to read the code – we’ll dive into relevant parts of it later on. I add it here just so you could get an impression:
That’s a pretty big function, right? Just looking at it causes some cognitive load. The thought of diving into such a long function might be discouraging. Anyway, this implementation makes the tests pass, so it works. Let’s summarize what’s happening there:
- It starts with setting local variables
- A loop to set the
pre-month
padding days. - A loop to set the month days.
- A loop to add the
post-month
padding days. - An
if
statement to see if we actually have these days and push them to the array. - Return the grid and the weekdays in the calendar grid object.
Lucky us the weekdays
are returned from a different function, otherwise we would have gotten ourselves with a much bigger function.
So… this function works. We can merge it, right?
Technically, yes.
…
What? Were you waiting for a “but”?
Ok, if you insist.
The Detail is in the Implementation
We should separate the implementation from the interface. The interface includes the function’s parameters, name, and returned value. The implementation of the algorithm is the details. The implementation detail.
If we change the implementation without changing the interface, it would mean we are doing a refactor.
I’m going to share with you another implementation. This implementation uses just one loop without the external if
. Is it clearer? Easier to read? Less intimidating? More considerate for future generations of developers? You’ll be the judge of that.
But wait! Can this small snippet replace three loops and an if
statement?
How to Make Sure a Refactor Didn’t Break My Code?
We made a pretty big change to the code. How can we be certain it still works the same?
Remember that we started by looking at the tests? This is where they shine. The tests ensure that if we change the interface while refactoring, we’ll get notified. Changing the interface is called “A Breaking Change”. Unintentionally changing the interface is called “A Bug in Production”. If we have tests in place, the warning is hard to miss:
Tell you a secret: I broke the API several times while working on this refactor. The thing is, I knew the job was well done only when the tests passed, so no regression left my own IDE.
With the tests in place, refactoring boils down to just pacifying the test gods. And they send a clear green sign to let you know you are ready:
Summary
Our interface is a sacred contract between us and our consumers. I have written several times about the importance of covering the public interface. I can’t stretch enough about the other side of that coin – test mostly your public interface.
If you don’t test your public interface, you are probably testing the implementation detail. This is bad for various reasons, such as the coupling pitfall. It is also a cause for frustration with many developers about “changing the tests every time the implementation changes”.
Except for a very gifted few, nobody writes a perfect code the first time. A few more people make it beautiful the second time. It usually takes me three iterations to make something I’m happy with.
The ability to refactor with confidence is only achieved with tests (or with overconfidence, but that’s a different topic :)). In this example we saw how extensive tests can be for just one crucial function – and they take a second to run on every code change. You decide if you want your tests to run automatically or manually on every change you make.
What do you think? Do you make sure to distinguish between the two? Can you think of more use cases where this is important? Or maybe problematic?
Thanks a lot to Benjamin Gruenbaum and Yuval Bar Levi for the kind and thorough review.