From an early point on in my career I was exposed to unit testing, in particular the JUnit framework. This was at a time when both the Java programming language and the theory of implementing automated tests were gaining a lot of traction. At the time, a strong mentor of mine took me under her wing and exposed me to designing testable code. Since then, my approach towards coding has significantly taken a different direction. I want to share a few thoughts here on this topic.
Apologies up front if any of this seems redundant, but I have personally been a witness to enough poorly designed code to make me question if I should make a career switch. My goal is hopefully share a bit of knowledge and drive a few thoughts into your design approach.
The hard part is deciding what to test. Time, resources, ROI are factors that determine the level of effort that you apply to your efforts.
It is tempting to focus a lot of effort on full stack or integration level testing. There are a number of open source and commercial tools available to help automate testing from an end user’s perspective. It is important to note however, this approach is extremely dependent on the state of the data. I am not knocking this approach, however high level testing approaches are dependent on many underlying pieces that make the system work. At the end of the day, high level testing is just a sanity check, and intended to ensure the end result meets expectations.
Personally, I think the best bang for your buck is testing at the lowest level, or what is referred to as unit testing, and that drives a conversation about how to structure your code so that it is testable.
It is often hard to sell other parties on the concept of creating automated tests. This takes additional time and effort. Unrealistic expectations of 100% coverage or somewhere near there might exist within a development team internally. It is easy to crank out code, ship it, and address the needs of the immediate ask. It requires more effort and dedication to deliver solid maintainable code. I get the mentality that you might move onto another position or organization in the next 2-3 years, and may not be personally invested in your deliverable. My advice there would be that you might want to take this aspect of development as personal growth. It might not be recognized in your current position, however will help you grow, and make you a better asset for future roles.
So with a focus on unit level testing, this will have a huge impact on how you structure your code. This basically requires that methods are coarse grained, broken down to small building blocks, this might sound over engineered to some, and for the rest you get it. I can usually tell when reading code if it is designed with testing in mind. Let‘s illustrate this with an example.
First, an illustration of what we are attempting to build, a basic grid representing a given calendar month, for which each given date two metrics driven by an API call are presented.
Let’s walk through a real world example of how to structure a function. In this example, we are working with a React based function responsible for displaying data in a calendar format. Here is the pseudocode.
In the above example, a series of operations are performed within a single method.
Obviously, a lot of details have been omitted, however you begin to see how the method is beginning to become verbose and one could only imagine how this would grow with a full implementation. In addition to what I would describe as an ‘bloated‘ method, this introducess a level of complexity when it comes to modularized testing.
Let‘s take a look at how this be broken further by introducing additional, finely grained methods.
With the above refactoring, you are now setup for concise unit tests, and single purpose methods.
A few learning points from my end...
I apoligize up front to anyone that is in the know, this might be redundant. But for a lot of people, why would we break this down into 5 different methods? Let‘s deep dive into that. Slightly aligning with SOLID principles, starting with the ‘S‘, or single responsibility, methods should be decomposed/disected into granlular elements. This sets us up for smaller testable units.