Revising Unit Testing For Frontend Engineers

Revising Unit Testing For Frontend Engineers

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

  1. 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.

  2. Write functional code as much as possible. They are much easier to test as they have the same outputs for the same inputs.

  3. 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).

  4. Install relevant Jest plugins for your IDEs.

🏓 More Resources

  1. Jest Documentation — https://jestjs.io/docs/getting-started

  2. Jest Expect - https://jestjs.io/docs/expect

  3. Jest SpyOn - https://jestjs.io/docs/jest-object#jestspyonobject-methodname

  4. Jest Mock Functions - https://jestjs.io/docs/mock-function-api

Credits

  1. Cover Image by mamewmy on Freepik

Did you find this article valuable?

Support Aditya Krishnan by becoming a sponsor. Any amount is appreciated!