You want to be a better developer, you should start writing tests. Tests will help you discover many improper habits that make your code not reusable or complicated to test. In this article, we will live code function that is impossible to test and implement changes that will make the function testable and reusable.
Testing will help you write better code and become a reliable senior developer.
Developers often say they want to write tests, but they do not know where to start. Start with motivation. Some would say they do not know how to convince product owners to spend time on testing over creating new features.
- 👍 code with tests is cheaper to maintain over time
- 👍 code with tests will allow you to adapt to market needs faster than competitors
- 🚦 try to estimate the cost of the bugs in production (lost revenue, bad reputation, cost of analysis, development, testing, and deployment) invest in testing infrastructure and tests if you can estimate the return of the investment is reasonable
Live Code, transforming to testable function
Following code snippet will show you a function try to test it.
If you see nothing wrong with this function this article is meant for you.
🚧 It is not possible to write a test for the function, do you know why?
- every time we will call this function it can return a different value
- the function uses global state when the new Date() is called
🚧 It is not possible reuse this function for shifting different number of days
- can not extend or compose this function
- the date is not provided as an argument
- the “+1” days is hardcoded and we cannot extend this function
Fist attempts to fix the example function
✅ Avoiding global state
- the date is provided to function as an argument
- the default value is provided as a call to new Date()
✅ Enabling reusability
- the days variable is provided to the function as an argument with
- default value `0`
🚨Avoid side effects like mutating outside or global variables
- you should not mutate objects provided by reference
- not even object in const declaration will help
Example function without side effects
Following code snippet will not only allow testing but it will create a more generic function that we can reuse.
✅ cloning an object is good practice, to avoid side effects
✅ in this case as a bonus, it allows us to provide a string to the function as a argument
Use function object destructuring for parameters with default values
Parameters with default values are optional. Required parameters have to be filled in and should appear before optional parameters.
Function with two optional parameters will require both parameters for overriding second parameter and make the first required. We can start polemics about which parameter should go first. We can follow simple rule for two or more parameters you should use function object parameter.
A simple function can be difficult to test. Thinking about how to write it in a way to enable testing can be an interesting exercise for a curious developer. Spoiled by the title of the article when you start to tests you will be tempted to follow Functional Programming rules. We have already used core principles of a Pure Function in the live code above.
If you want to read more about Functional Programming and Pure Functions please read full article from Arne Brasseur. I will add shortened parts from his article.
A pure function is a function where the return value is only determined by its input values, without observable side effects.
When a function performs any other “action”, apart from calculating its return value, the function is impure.
Single impure function would contaminate any function that calls it.
Keep State Local
A pure function can only access what you pass it, so it’s easy to see its dependencies. We don’t always write functions like this. When a function accesses some other program state, such as an instance or global variable, it is no longer pure.
Take global variables as an example. These are typically considered a bad idea, and for good reason. When parts of a program start interacting through globals, it makes their communication invisible. There are dependencies that, on the surface, are hard to spot. They cause maintenance nightmares. The programmer needs to mentally keep track of how things are related and orchestrate everything just right. Small changes in one place can cause seemingly unrelated code to fail.
When functions are pure and values are easy to inspect and create, then every function call can be reproduced in isolation. The impact this has on testing and debugging is hard to overstate.
source for Pure Function, Keep State Local and Reproducible Results: [Arne Brasseur] https://www.sitepoint.com/functional-programming-pure-functions/
Standard built-in impure functions and substitution:
- date object methods
- 👎 Array.pop() 👍 const newArray = array.slice(0, array.length — 1);
- 👎Array.push() 👍 const newArray = […array, item];
- 👎 Array.shift() 👍 const [first, …rest] = array;
- 👎 Array.unshift() 👍 const newArray = [item, …array];
- 👎 Array.splice(), 👍 Array.slice()
- 👍 Array.concat() or […array] to clone Array if the following function is impure
- 👎 Array.sort(), 👍 const sortedArray = array.concat().sort() or const sortedArray = […array].sort();