I am republishing this blog on Unit Testing by keeping it quick and to the point. This is for those who already have some idea about unit testing but need some brushing up. Beginners could also use this blog as a starting point for multiple important concepts. I use Jest for explanations as it is based upon robust unit testing concepts. Let's start.
🤷 Why Write Unit Tests?
You get excellent protection against regressions because when a unit test fails, it pinpoints where your code is failing
It makes refactoring possible because you can modify code freely. You have unit tests to back you up. They will tell you if some feature fails before you push your code.
You learn about the quality of the code you have written because hard-to-test code is usually hard-to-understand, poorly designed or poorly architected code.
🤔 What Should Unit Tests Do?
I use Jest to write Unit Tests. It is one of the most popular libraries and can do many things:
Check the return value of a function
Check the return type of function
Check if a function called another function
Check how many times a function was called
Check if the returned array/object contains or doesn’t contain something and many more
Should we check all of this in every unit test? Of course not! We should only test those that are the essence/specification of the function.
If you think that for a set of inputs, the output should be a particular value. Then put a test for that.
If you think that your function should call this particular other function for correct functioning, then put a test for that.
If a function should call another function with a particular set of arguments then check for that.
🎣 Tell Me How To Do It
Jest provides hundreds of functions to test your functions. I will tell you about some major ones.
⚡️ Test & Describe
test
represents either the whole or part of a specification that should be tested.
For example, check if a function returns z
for inputs x
& y
or check if an object’s property has changed after calling some function.
If one function has multiple cases to test, separate them into different test
functions but keep them inside a common describe
function. describe
helps with organization and you can run individual describe
blocks separately. Similarly, keep all functions of a particular file or module in their own describe
functions. This will help narrow down issues faster.
⚡️ Expect
expect
- It means to check something with something.
Eg. expect(A).toEqual(B)
.
Usually expect(receivedResult).toEqual(expectedResult)
Read more about all equality checks here — jestjs.io/docs/using-matchers
⚡️ Spy On
jest.spyOn
- You can spy on a function to check if it was called or even make it do something else just for a particular test. For example, you can mock a function’s return value or implementation.
⚡️ Mock Implementation
We should test only the functionality of the function in question. Do not test the internal workings of other functions that your function calls. Make sure to mock all functions other than your function such that it wouldn’t break because of changes in those other functions.
const getOpacitySpy = jest.spyOn(someModule, "getOpacity").mockImplementation((object) => 1);
Jest.spyOn
is used to create a mock function but also tracks calls made to that function. Here, I set a spy on a function getOpacity
which is inside someModule
, getOpacity
should ideally return the opacity of an object. However, I changed its implementation such that it always returns 1. You can write anything in the implementation. The same thing can be achieved with mockReturnValue
as well.
const getOpacitySpy = jest.spyOn(someModule, "getOpacity").mockReturnValue(1);
Do not forget to mockRestore
after you do the expect
, otherwise, the spy will remain for all remaining tests as well.
getOpacitySpy.mockRestore();
🌀 Show me an Example
If function A calls functions B and C, mock B and C before testing A because if you test the functionality of B or C in the test for A, this means that anytime B or C changes, you will have to make changes in 2 unit tests, which just increases work. You can mock B and C using the mockImplementation
method.
const A = (a) => {
const b = B(a);
const c = C(b);
return c;
}
test('testing if A calls B and C', () => {
const BSpy = jest.spyOn(someModule, "B").mockImplementation();
const CSpy = jest.spyOn(someModule, "C").mockImplementation();
A();
expect(BSpy).toHaveBeenCalledTimes(1);
expect(CSpy).toHaveBeenCalledTimes(1);
BSpy.mockRestore();
CSpy.mockRestore();
});
In this example, we mock the implementations of B and C. But we check that they are called while testing A because we know that calling them is essential for the working of A, but their internal workings are not essential for testing A. Their internal workings should be tested while testing B and C separately.
🌠 Tips & Tricks
Make sure to mock implementations of every other function other than your function. Otherwise, you are at risk of breaking your unit test because someone else changed their function.
Write functional code as much as possible. They are much easier to test as they have the same outputs for the same inputs.
Make sure your functions do only one thing. If it does multiple important steps, separate them into more functions, so that you can test them separately. Otherwise, your test specification will get complex (which increases cyclomatic complexity and is bad).
Install relevant Jest plugins for your IDEs.
🏓 More Resources
Jest Documentation — https://jestjs.io/docs/getting-started
Jest Expect - https://jestjs.io/docs/expect
Jest SpyOn - https://jestjs.io/docs/jest-object#jestspyonobject-methodname
Jest Mock Functions - https://jestjs.io/docs/mock-function-api
Credits
- Cover Image by mamewmy on Freepik