ABC... Tests ⚙️
Tags: testing, jest, vitest
Published: 2023-06-02 06:14:28

> _Unit testing for Web development_

_**NB**: This post follows a talk on the same topic; here is the support of this presentation: [https://slides.com/damienchazoule/abc-tests](https://slides.com/damienchazoule/abc-tests/#1)_

----------

## Once Upon A Time...

Unit testing are the job of all developers, whether they are _frontend_ or _backend_ experts (or even _fullstack_).

In 1994, the first unit tests were created for the SmallTalk language: SUnit.

3 years later, <span style="text-decoration:underline;">Kent Beck</span> (xUnit's creator) meets Erich Gamma and brings SUnit to the Java language, called **JUnit**!

At the beginning of the 2000s (more exactly 2001), the xUnit family grows with the arrival of a new one: **JSUnit**.

The goal of unit tests is to verify the proper functioning of all or part of an application / a development. In the same way, these tests can indicate possible post-delivery issues of a project (during the maintenance phase).

Unit tests are part of Agility, especially with the e**X**treme **P**rogramming (**XP** for friends) method which includes the TDD pattern during the iterations of a project. **T**est **D**riven **D**evelopment means writing unit tests even before the development of functionalities / technical components, as well as comments in the code and / or technical documentation.

## Different Kinds Of Tests

There are several "degrees" / levels of unit testing, but each works the same way:

- Setting up the test environment
- Executing the test / unit test suite
- Validation / control of test results
- Finally, resetting the test environment

The 1st tests to perform ("simple" / "basic" tests) are to test the functions, the algorithms, the business logic in the code.

_**NB**: This kind of test helped my son (6 years old at the time) to understand that it's possible to add 2 numbers and get a negative result. Even a `sum()` function can be fun to test_ 😉

The 2nd degree of unit testing concerns component-oriented frameworks (Angular, React, Vue, etc...). These tests aim to validate the behavior of a single component / or a set of components (in a complex context, the router for example). Here, we'll get closer to the **BDD*** pattern.

Finally, there are end-to-end (**E2E**) tests to verify a user journey / business use case. For example, if you need to check the correct operation of a form, in order to obtain an estimate, or something else... These kinds of tests are there for that!

## Simple / Basic Testing

> _Make your choice...._

There are many unit testing solutions in JavaScript. Promoted by the AngularJS (**R.I.P**) ecosystem, in the 2010s, the [Karma](https://karma-runner.github.io) test engine and its assertion library [Jasmine](https://jasmine.github.io) had their heyday. Note that Jasmine is neither more nor less than the evolution of JSUnit!

At the same time, the [Mocha](https://mochajs.org) testing framework started to be talked about, an alternative solution to Karma which interfaces with any assertion library. Some time later, after the AngularJS fork (led by <span style="text-decoration:underline;">Evan You</span>), Vue will embed the Mocha x [Chai](https://www.chaijs.com) pair as the default testing solution for its component-oriented framework.

With React, Facebook has redesigned a new unit testing solution that is much simpler in terms of configuration. [Jest](https://jestjs.io), by the way, is far from being a "joke", since it carries both the test engine and the assertion library. It is "THE" all-in-one solution for those who want to test their application / code, because of its simplicity (but also its speed).

At this point, I would have already told you to choose Jest without hesitation. But in 2023, the new generation of JavaScript libraries is here! of course, I want to talk about Vite, but especially [Vitest](https://vitest.dev). It's all in the name 🚀 This last one interfaces perfectly with the Vite bundler, and takes the main lines of its predecessor Jest.

### How To Test ?

Let's get practical, by creating a new JavaScript project (with the next-gen bundler):

```sh
npm create vite@latest abc-tests -- --template react-swc
npm install --save-dev vitest
```

_**NB**: Above, I scaffold the project with the React framework + **S**peedy **W**eb **C**ompiler for a faster compilation than with Babel!_

Let's start slowly by creating a 1st utils.js file, then by writing a simple function:

```js
/**
 * Format date to 'dd/mm/yyyy' pattern
 *
 * @param {Date} dateToFormat
 * @returns {string} Formatted date
 */
export const formatDate = (dateToFormat) => {
  if (dateToFormat instanceof Date) {
    const dd = dateToFormat.getDate();
    const mm = dateToFormat.getMonth() + 1;
    const yyyy = dateToFormat.getFullYear();

    return `${dd}/${mm}/${yyyy}`;
  }

  throw new Error('Not a Date');
};
```

<figcaption>utils.js</figcaption>

Now, let's test this 1st formatting function from the code below, and the following terminal instruction: `npx vitest`

```js
import { describe, it, expect } from 'vitest';
import { formatDate } from './utils';

describe('formatDate', () => {
  it('should returns formatted date', () => {
    const mayTheFourth = new Date('2023-05-04');
    expect(formatDate(mayTheFourth)).toEqual('21/5/2023');
  });

  it('should throws an error', () => {
    expect(formatDate('2023-05-04')).toThrowError('Not a Date');
  });
});
```

<figcaption>utils.test.js</figcaption>

And that's it, it's relatively simple after all! No ? Here, both test cases are covered, i.e. on success, but also on error when the date isn't of type `Date`. Here are some other examples:

```js
import { suite, test, expect } from 'vitest';
import { toCapitalize } from './utils';

suite("concat or evaluate, that's the question", () => {
  test('it should concat 2 values', () => {
    expect('11' + 1).toEqual('111');
  });

  test('it should evaluate 2 values', () => {
    expect('11' - 1).toEqual(10);
  });
});
```

_**NB**: There are some keywords to remember when it comes to frontend-oriented unit testing. The `describe` and `suite` keywords allow you to declare a test suite, usually associated with a feature. The keywords `it` and `test` are relatively self-explanatory, it's simply the test. Finally, `expect`, the main one / the assertion! Note that `it` is more used than `test` because of the following word association "it should..."; it works less well with "test"_ 😋

It is obviously possible to test asynchronous functions, especially in the context of API testing. Here is a simulation of a service function:

```js
/**
 * Get next value from current
 *
 * @param {number} currentValue
 * @returns {Promise} { nextValue }
 * @throws {Error} NaN
 */
export const getNextValue = (currentValue) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof currentValue === 'number') {
        const nextValue = currentValue + 1;
        resolve({ nextValue });
      } else {
        reject(new Error('Not an Number'));
      }
    }, 1000);
  });
};
```

<figcaption>counterService.js</figcaption>

And here are the associated tests, in case of success, as well as in case of error:

```js
import { describe, it, expect } from 'vitest';
import * as CounterService from './counterService';

describe('CounterService', () => {
  it('should returns next value', async () => {
    const { nextValue } = await CounterService.getNextValue(41);
    expect(nextValue).toEqual(42);
  });

  it('should throws an error', async () => {
    expect(await CounterService.getNextValue('41')).toThrowError('Not an Number');
  });
});
```

<figcaption>counterService.test.js</figcaption>

Some tests may seem trivial or even obvious at first glance, but this is rarely the case. In the context of mapping functions (between backend and frontend), the interest is great, since it makes it possible to quickly diagnose regressions / changes at the API signature level. I therefore strongly advise you to split your code as much as possible in order to facilitate testing.

## Component Testing

> _Make your choice (again)..._

This is where the **B**ehavior **D**riven **D**evelopment* pattern makes sense.

Just as for "classic" unit tests, there are several solutions for testing the components of your application.

Historically, AirBnB comes first with [Enzyme](https://enzymejs.github.io/enzyme)! Equipped with an API close to jQuery (to manipulate components), Enzyme allows to mount / render a React component in a virtual DOM (the famous JSDOM). This unit testing framework has a `shallow` mode allowing to mount a single component (without children), so in a "unitary" way; and a `mount` mode to render the parent component and the child components associated with it, and thus technically to reach the `componentDidMount` / `componentDidUpdate` functions of the React component lifecycle.

Enzyme's direct competitor is none other than [Testing Library](https://testing-library.com). Arrival later than this last one. Testing Lib(rary) has a more explicit (and simpler: `queryBy*` / `getBy*` / `findBy*`) modern API for rendering a component and its children through a single function. This library aims to provide better testing practices for component-oriented frameworks. In fact, Testing Lib is agnostic and can be used with Angular, React, Vue, Svelte, Cypress, etc...

### How To Test ?

Once again, let's get down to practice by adding the required dependencies and setting up the test environment:

```sh
npm i -D @testing-library/jest-dom @testing-library/react jsdom
```

```js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';

// https://vitejs.dev/config/
export default defineConfig({
  test: { environment: 'jsdom' }, // 👈 TODO
  plugins: [react()]
});
```

<figcaption>vite.config.js</figcaption>

Consider the (default) `<App />` component below:

```jsx
import { useState } from 'react';
import * as CounterService from './counterService';
import styles from './app.module.css';

export default function App({ defaultValue = 0, ...props }) {
  const [count, setCount] = useState(defaultValue);

  const handleClick = async (currentValue) => {
    if (props.onClick) props.onClick({ prevValue: currentValue });

    try {
      const { nextValue } = await CounterService.getNextValue(currentValue);
      setCount(nextValue);
    } catch (err) {
      // eslint-disable-next-line
      console.log(err.message);
    }
  };

  return (
    <div className={styles.app}>
      <h1>Vite + React</h1>
      <div className={styles.card}>
        <button onClick={() => handleClick(count)}>Increment</button>
        <p>Count: {count}</p>
      </div>
    </div>
  );
}
```

<figcaption>App.jsx</figcaption>

Next, let's run some "behavior" oriented tests:

```jsx
import { expect, describe, afterEach, it, vi } from 'vitest';
import { cleanup, render, screen, fireEvent } from '@testing-library/react';
import matchers from '@testing-library/jest-dom/matchers';
import App from './App';

expect.extend(matchers);

describe('<App />', () => {
  afterEach(cleanup);

  it('should renders', () => {
    render(<App />);
    expect(screen.getByText('Vite + React')).toBeInTheDocument();
  });

  it('should match snapshot', () => {
    const container = render(<App />);
    expect(container).toMatchSnapshot();
  });

  it('should increment the count value', async () => {
    render(<App defaultValue={41} />);
    expect(screen.getByText('Count: 41')).toBeInTheDocument();

    const btn = screen.getByRole('button', { name: 'Increment' });
    fireEvent.click(btn);

    expect(await screen.findByText('Count: 42')).toBeInTheDocument();
  });

  it('should triggers the click event', () => {
    const onClickMock = vi.fn();
    render(<App defaultValue={41} onClick={onClickMock} />);

    const btn = screen.getByRole('button', { name: 'Increment' });
    fireEvent.click(btn);

    expect(onClickMock).toHaveBeenCalled();
    expect(onClickMock).toHaveBeenCalledWith({ lastCount: 41 });
  });
});
```

<figcaption>App.test.jsx</figcaption>

Above, several test cases:

- The first `should renders` simply renders a component and checks its content;
- The second `should match snapshot` creates an image of your component and allows you to detect any differences when your component evolves. In this specific case, you will have to update your `snapshot`;
- The 3rd test case checks the value of the `count` variable before and after clicking on the button;
- Finally, in the 4th test case, we inject a "mocked" function to our component, we click on the button, then we check that this function has been called with the right arguments;

`fireEvent` does a lot of things related to events in the DOM, such as filling the fields of a form. For simple cases, why not... For more complex cases, better leave that to end-to-end testing...

## Mock Service Worker (MSW)

It's impossible to talk about API unit testing without talking about [Mocker Service Worker](https://mswjs.io). MSW acts as a middleware between your application and APIs, intercepting requests at the network level. So, it is no longer necessary to mock Axios or Fetch to control the algorithms related to API calls and returns.

MSW supports REST(ful) requests and GraphQL queries, and offers two modes: `worker` mode (ideal for developing and debugging your application), and `server` mode for _backend_ and unit testing.

Straight from the documentation, here's how to install and initialize the `worker`:

```sh
npm i axios && npm i -D msw
npx msw init public --save
```

Let's enhance the "fake" service previously created by adding a function calling the "real" service:

```js
import axios from 'axios';

/**
 * Post current value to get next
 *
 * @param {number} currentValue
 * @returns {Promise} { nextValue }
 * @throws {Error} 40x or 50x
 */
export const postCurrentValue = async currentValue => {
  try {
    const response = await axios.post('/api/counter', { currentValue });
    return response.data; // { nextValue }
  } catch {
    throw new Error('Not an 20x');
  }
};
```

<figcaption>counterService.js</figcaption>


_**NB**: Below, here is the code to execute at the entry point (`index.js`) of your project to mock the API `/api/counter` return, when running the application._

```js
import { rest, setupWorker } from 'msw';

const handlers = [
  rest.post('/api/counter', async (req, res, ctx) => {
    const { currentValue } = await req.json();

    if (typeof currentValue === 'number') {
      const nextValue = currentValue + 1;
      return res(ctx.delay(1000), ctx.json({ nextValue }));
    }

    return res(ctx.status(422, 'Unprocessable Entity'));
  })
];

const worker = setupWorker(...handlers);
worker.start();
```

And now, here is how the `server` mode (alternative to the `worker` mode) works in the context of unit tests (always in the case of success, but also of failure):

```js
import { describe, beforeAll, afterEach, afterAll, it, expect } from 'vitest';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import * as CounterService from './counterService';

describe('postCurrentValue', () => {
  const server = setupServer();

  beforeAll(() => server.listen());
  afterEach(() => server.resetHandlers());
  afterAll(() => server.close());

  it('should returns next value', async () => {
    server.use(
      rest.post('/api/counter', (req, res, ctx) => {
        return res(ctx.json({ nextValue: 42 }));
      })
    );

    const result = await CounterService.postCurrentValue(41);
    expect(result.nextValue).toBeDefined();
  });

  it('should throws an error', async () => {
    server.use(
      rest.post('/api/counter', (req, res, ctx) => {
        return res(ctx.status(400, 'Bad Request'));
      })
    );

    const err = await CounterService.postCurrentValue('41').catch(err => err);
    expect(err.message).toEqual('Not an 20x');
  });
});
```

<figcaption>counterService.test.js</figcaption>

## End-To-End Testing

> _Make your choice (again and again)..._

For more complex tests, especially business use cases, there is end-to-end testing. The interest of these tests is to prove the proper execution of a functional course, and to play your application in real cases / close to the final use.

To do this, [Cypress](https://www.cypress.io) has become a key player in end-to-end testing. With a detailed documentation and an active community, this framework makes it possible to emulate a user journey, directly in a browser (Chrome, Firefox or Edge) and to follow its progress visually. With the arrival of Testing Library, Cypress has enhanced its API, and now has `getBy*` functions (`getByText`, `getByRole`, etc...) to manipulate DOM elements.

As an alternative to Cypress, there is [Playwright](https://playwright.dev/). Supported by Microsoft, it offers a simple and fast configuration for end-to-end testing. Unlike Cypress, Playwright plays business use cases from one or more "headless" browsers (Chromium, Firefox, Webkit) in parallel. It is also available in several languages: JavaScript, Java, Python and C# (unlike its counterpart). Finally, this framework benefits from an API similar to Testing Lib since October 2022, to the delight of developers... 🙏

### How To Test ?

What could be more concrete than showing you Playwright in action. To do so, let me demonstrate the power of codegen mode:

```sh
npm install --save-dev @playwright/test
npx playwright install
npx playwright codegen --viewport-size=800,600 http://localhost:5173
```

_**NB**: `npx playwright install` allows you to install the "headless" browsers on which you will run your tests, namely Chromium, Firefox and WebKit._

![CodeGen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ldevrjzg57sjqhvfgfnw.png)

_**NB**: The DIY Redux application is a development made as part of work around State Management (i.e. [React Vs. Vue](https://dev.to/dmnchzl/diy-state-management-react-vs-vue-4nja))._

Playwright's `codegen` mode is magic! It allows to simulate the functional test case, while recording each of your actions. Then just adapt the selectors, add assertions according to your needs, and run Playwright again (`playwright test`) to have a complete end-to-end test case.

```js
import { test, expect } from '@playwright/test';

test.use({
  viewport: {
    height: 600,
    width: 800
  }
});

test('it should fill form and move to next (then to next)', async ({ page }) => {
  await page.goto('http://localhost:5173/');

  await expect(page.getByRole('heading', { name: 'Identity' })).toBeVisible();

  await page.getByPlaceholder('Abramov').click();
  await page.getByPlaceholder('Abramov').fill('Chazoule');

  await page.getByPlaceholder('Dan').click();
  await page.getByPlaceholder('Dan').fill('Damien');

  await page.getByRole('button', { name: 'Next' }).click();

  await expect(page.getByRole('heading', { name: 'Professional' })).toBeVisible();

  const firstComboBox = await page.getByRole('combobox').nth(0);
  firstComboBox.selectOption('Front');

  const secondComboBox = await page.getByRole('combobox').nth(1);
  secondComboBox.selectOption('JavaScript');

  await page.getByPlaceholder('Go, Rust...').click();
  await page.getByPlaceholder('Go, Rust...').fill('TypeScript');

  await page.getByRole('button', { name: 'Next' }).click();

  await expect(page.getByRole('heading', { name: 'Fantasy' })).toBeVisible();

  await page.getByPlaceholder('Cliff').click();
  await page.getByPlaceholder('Cliff').fill('Ragnar');

  await page.getByRole('button', { name: 'Previous' }).click();
  await expect(page.getByPlaceholder('Go, Rust...')).toHaveValue('TypeScript');

  await page.getByRole('button', { name: 'Next' }).click();
  await expect(page.getByPlaceholder('Cliff')).toHaveValue('Ragnar');
});
```

## Code Coverage

What about code coverage in all this? 🤔 Code coverage is a metric that determines the code rate performed. This indicator highlights the percentage of code statements, covered branches, executed functions, as well as lines of code not covered (explicitly).

Be careful! It is important to differentiate between tests and the coverage rate! Indeed, the code coverage rate is calculated according to the unit tests executed, but a successful test does not necessarily mean that the code is entirely covered... In the same way, we must also ask ourselves about the interest of an optimal coverage; 100% coverage is sometimes utopian or pointless if it does not bring any added value to the project (security, scalability, maintainability).

With Vitest, the code coverage tool is not included by default, unlike Jest, which relies on Babel to provide this functionality, without installing any additional dependencies.

However, it is possible to obtain code coverage using the C8 library:

```sh
npm i -D @vitest/coverage-c8
npx vitest --coverage
```

```sh
-------------------|---------|----------|---------|---------|---------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line
-------------------|---------|----------|---------|---------|---------------
All files          |   96.73 |    93.33 |     100 |   96.73 |
 App.jsx           |    92.1 |    83.33 |     100 |    92.1 | 16-18
 counterService.js |     100 |      100 |     100 |     100 |
 utils.js          |     100 |      100 |     100 |     100 |
-------------------|---------|----------|---------|---------|---------------
```

So, the result above highlights the fact that most of the lines are covered, except for lines 16 to 18 of the `App.jsx` file (according to previous tests). Looking closer, we see that it is a `console.log()`. What's the point of going through here? Is there any code missing!? Or is it normal...

## The Final Word

Very often, unit tests are synonymous with extra costs for the project (especially for frontend projects because they do not guarantee data integrity, unlike the backend). This is both true and false... Unit testing is a habit to be acquired. The more developers get used to testing their code, the less it will cost the project. In the same way, the flex of the unit test, makes it possible to better split its algorithms, to better organize its technical projects, and so to guarantee a better maintainability of the source code.

The mistake not to make on the frontend is to leave a project "abandoned" without regular maintenance (more than 6 months) and especially without testing (nor comments)... The Web evolves quickly, frontend projects must be updated at the same frequency. In the same way, unit tests guarantee a level of quality and understanding of the code! So the durability of a Web project depends in part on its unit tests 👌

To the question "is it doubtful to test?", I answer that I don't doubt anymore! I test my developments because it's natural (and fun) 🙂

#### To Go Further...

In order to provide good quality code for your frontend projects, it is possible to interface the SonarQube quality measurement tool with Vitest. In fact, Vitest is able to provide two types of reports: coverage reports and test reports. By combining these two tools with your CI/CD (GitHub Actions, GitLab CI, etc...) you will be able to guarantee some level of quality, by defining a coverage rate to be reached (although controversial) but above all by ensuring that the unit tests are working properly and automatically 👍

_**NB**: SonarQube is a relatively visual tool, and one that allows project actors (other than developers) to quickly identify the robustness of the generated code (red / green indicators)._

Finally, we have already seen the "classic" unit tests, component-oriented tests, but also end-to-end tests; there is a last category of tests: the Mutation Testing! The goal of these kinds of tests is to perform a multitude of "faulty" use cases to know if our application is robust. At the moment, I have only found one tool to do that. The JavaScript ecosystem will continue to evolve in the next few years, so we can expect to see this kind of test become more common. To be continued...