Unit Testing - A quick guide from Jr to Jr

Unit Testing - A quick guide from Jr to Jr

ยท

5 min read

Application development is a growing market around the world, more so since the pandemic. Given this swing, the demand for professionals of all levels is also increasing in ways that the current quantity of qualified professionals cannot fulfill. In such a scenario, stakes are high and bugs are common. In the light of that, not only must developers be able to code, but they must also guarantee that code does what it aims to. Make proof of it. That is why I have made this guide, to present some ways you can test your code and ensure its effectiveness.

The structure of a test

To illustrate this content in a practical way I have opted for using the Jest library in use case samples. My choice is based on its current popularity and the considerable percentual of adherence to Javascript-based frameworks. Jest is a library that offers an environment in which we can verify that Javascript code is having the expected behavior inside a system. The main structure consists of three parts:

  • a description of the unit being tested;

  • a description of general expectations for a code block;

  • and some markers of behavior expectation in punctual moments of the test.

Below we have a skeleton sample of this structure. "Describe", "it" and "expect" are the three parts respectively.

describe("UNIT DESCRIPTION", () => {
    it("block expectation description", () => {
        expect().toBeValid();
    });
});

Testing Functions

To begin with, we will look at a function to validate an email. The function receives a value and tests it in a RegEx pattern and returns a boolean. Below is the function:

export const emailValidator = (value) => {
  const pattern = /^([a-z\d.-]+)@([a-z\d-]+)\.([a-z]{2,8})(\.[a-z]{2,8})?$/;

  return pattern.test(value);
};

In this case, the correct behavior will return true when a valid value matches the email pattern and false opposingly. Following such an idea, we can check the value coming in and out of it as the example:

describe("emailValidator", () => {
    it("should return true if email is valid", () => {
        const email = "valid@email.com";
        expect(emailValidator(email)).toBeTruthy();
    });

    it("should return false if email is invalid", () => {
        const email = "invalid/email.co@";
        expect(emailValidator(email)).toBeFalsy();
    });
});

This first sample covers the cases of strings with valid and invalid values. Were we making a more efficient test, we could and should furnish plenty of cases with a wide variety(undefined, null, etc...), but for now, focus on the markers of behavior expectation. Though they are limited, hardly will we meet cases it will not help.

Testing Elements in DOM

Although Jest is a handful tool, it can be used together with other tools to extend the coverage of testing. For the next case, check this Login Popup made out of React Js framework:

import LogoImage from "../assets/images/logo.svg";
import { emailValidation } from "../utils/validation.js";

const LoginPopup = ({ loginFunction }) => {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");

    const didLogin = () => {
        if (emailValidation(email)) {
            loginFunction(email, password);
        }
    };

    return (
        <div>
            <label htmlFor="email">
                <h5>Email</h5>
            </label>
            <input type="text" onChange={(e) => setEmail(e.target.value)} value={email} id="email" />
            <label htmlFor="password">
                <h5>Password</h5>
            </label>
            <input type="password" onChange={(e) => setPassword(e.target.value)} value={password} id="password" />
            <button onClick={() => didLogin()}>Confirm</button>

            <img
                src={LogoImage}
                style={{
                    margin: "10",
                    width: "40",
                    height: "40",
                    objectFit: "cover",
                }}
                alt="Logo"
            />
        </div>
    );
};

export default LoginPopup;

To test this component, I have used the tool "render" from the Testing Library for React. It allows us to render as a way of checking the rendering of elements presented by the component. It is applied in the following way:

import { render } from "@testing-library/react";

import LoginPopup from "./LoginPopup";

describe("LoginPopup", () => {
    it("should have a logo with specified style.", () => {
        const { getByAltText } = getRenderer();

        expect(getByAltText("Logo")).toHaveStyle({
            margin: "10",
            width: "40",
            height: "40",
            objectFit: "cover",
        });
    });
});

function getRenderer() {
    return render(<LoginPopup />);
}

Once we call the "render" function as requested in TL documentation, we can still identify the main structure of Jest. The use of the Testing Library is more specifically connected to the capturing of the element rendering.

Moreover, in the following sample, I demonstrate a simplified way of repeating tests. The "each" structure serves to repeat the test to each of the positions in its internal array. It also accepts an array of variables in its position, by the way.

import { render } from "@testing-library/react";

import LoginPopup from "./LoginPopup";

describe("<LoginPopup />", () => {
    it.each(["Email", "Password"])("should have a heading '%s'.", (expected) => {
        const { getByRole } = getRenderer();

        expect(getByRole("heading", {name: expected})).toBeInTheDocument();
    });
});

function getRenderer() {
    return render(<LoginPopup />);
}

In this one, I have used "each" to capture both headings of our component. As I pass the expected value in the name property, the test will individually look for a match.

Testing Interactions in DOM

At last, I present a more elaborated sample in which I have used the userEvent, also from Testing Library, that makes it possible to test user Interactions with the DOM.

In this case, I verify that a certain login function is called when a user types a valid email and clicks the Confirm button. An action that would also trigger a page redirect.

import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import LoginPopup from "./LoginPopup";

describe("<LoginPopup />", () => {
    it("should call user login and change rendering if typed valid email and clicked in button.", (expected) => {
        const signInWithEmail = jest.fn()

        const { getByRole, queryByRole } = getRenderer({signInWithEmail});

        userEvent.type(getByRole("textbox"), "valid@email.com")
        userEvent.click(getByRole("button", { name: "Confirm"}))

        expect(signInWithEmail).toBeCalledTimes(1);
        expect(queryByRole("textbox")).not.toBeInTheDocument();
    });
});

function getRenderer({signInWithEmail}) {
    return render(<LoginPopup loginFunction={signInWithEmail} />);
}

Here I used Jest's mock function "fn" to avoid needing requests for an external database. As the function is passed to the component, I make use of the "userEvent" for both typing and clicking. Note the use of ".not" included in the expected behavior marker to make the opposite consideration and conclude the test as the verification of the lacking textbox indicates the change in rendering.

To put it in a nutshell, this is basically how the logic of unit testing goes, and as tests grow there is always the need for refactoring and removing older tests that end up being covered by newer ones. If you have doubts or any comments, do not hesitate in sharing. ;)

ย