Skip to content

[api] migrate testing framework (Jest > Vitest)#3555

Merged
DafyddLlyr merged 5 commits intomainfrom
api-migrate-jest-vitest
Aug 29, 2024
Merged

[api] migrate testing framework (Jest > Vitest)#3555
DafyddLlyr merged 5 commits intomainfrom
api-migrate-jest-vitest

Conversation

@freemvmt
Copy link
Member

@freemvmt freemvmt commented Aug 24, 2024

This PR is motivated by Jest's failure to handle ESM modules sufficiently well, which was a blocker on the Microsoft SSO work (ticket). In the end we decided to migrate away from Jest, with Vitest being the obvious alternative, because a quick experiment with it showed promise (see this comment).

Ticket: https://trello.com/c/N1egz8at

So, this PR does the following:

  • Add vitest package and configure (e.g. vitest.config.ts, setup files, package.json scripts etc)
  • Adapts all tests in api.planx.uk to use vitest instead of jest - this was the most laborious step and I will document some of the thornier aspects in my own review shortly (Vitest's dedicated migration guide was very useful)
  • Ensures coverage works similarly (e.g. relies on istanbul, keeps existing thresholds, displays in browser etc)
  • Removes all things Jest

A note on coverage

The text-summary coverage results are slightly different in this branch compared with main.

main (jest)

Statements   : 78.74% ( 2489/3161 )
Branches     : 54.96% ( 299/544 )
Functions    : 67.67% ( 312/461 )
Lines        : 77.95% ( 2305/2957 )

api-migrate-jest-vitest (vitest)

Statements   : 71.94% ( 1726/2399 )
Branches     : 54.92% ( 413/752 )
Functions    : 67.47% ( 305/452 )
Lines        : 71.75% ( 1659/2312 )

I haven't seriously investigated the cause of this discrepancy because it doesn't seem a substantial change. My instinct is that it's due to the default exclusions of each framework - e.g. as far as I can see, vitest has a long list of exclusions, whereas jest just ignores node_modules (or perhaps also excludes the istanbul defaults). But it might be some other implementation detail.

If we wanted to try and create a culture of always-increasing coverage, we could adopt the autoUpdate flag, as per my comment in vitest.config.ts. But for now I've stuck with the singular minimum coverage threshold of 55% of functions.

@freemvmt freemvmt requested a review from a team August 24, 2024 17:19
Copy link
Member Author

@freemvmt freemvmt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments for easier digestion!

post: jest.fn(),
}));
const mockAxios = Axios as jest.Mocked<typeof Axios>;
describe("Creation of scheduled event", () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapped tests which I adapted in a describe if it didn't already exist 🫡

}));
const mockAxios = Axios as jest.Mocked<typeof Axios>;
describe("Creation of scheduled event", () => {
vi.mock("axios", async (importOriginal) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vitest has this importOriginal argument which is passed to the factory constructor in a mock call - we can use this to import the actual module (replacing jest.requireActual), as per the docs.

Could also have used vi.importActual here, but using the arg seemed neater.

vi.mock("axios", async (importOriginal) => {
const actualAxios = await importOriginal<typeof import("axios")>();
return {
default: {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vitest does not assume, like jest, that a returned object is the default export of given module, so we have to be explicit about it, as per the docs.


describe("CSV data admin endpoint", () => {
afterEach(() => jest.clearAllMocks());
afterEach(() => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hooks like afterEach have to return null or undefined, so have remove implicit return, as per docs

// Please comment in and run locally if making changes to /roads functionality
describe.skip("fetching classified roads data from OS Features API for any local authority", () => {
jest.setTimeout(10000);
vi.setConfig({ testTimeout: 1000 });
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See relevant docs

// Assert
expect(result).toHaveProperty(key);
layersToRollup.forEach((layer) => {
// Jest can handle paths using dot notation, so keys with a dot need to be wrapped in []
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this was supposed to say 'cannot handle...' ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think "can" was correct here - the key here would have been something like a.b.c which Jest would have tried to default to a nested path, as opposed to the object in question which is likely { "a.b.c": "x" }

Either way, it's a good call to remove this comment - it looks like vitest handles this more gracefully - the test actually passes with or without the wrapping [] on line 154.

import { createScheduledEvent } from "../../../../lib/hasura/metadata/index.js";
import { queryMock } from "../../../../tests/graphqlQueryMock.js";
import { flowWithInviteToPay } from "../../../../tests/mocks/inviteToPayData.js";
import { MockedFunction } from "vitest";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no vi namespace so we have to explicitly import types like this one, as per docs

};

const mockSend = jest.fn();
jest.mock<typeof SlackNotify>("slack-notify", () =>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tsc/vitest seem smart enough not to need the type declaration anymore 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that's awesome because this step always felt awkward and brittle in jest imho 🙌

"test:watch": "TZ=Europe/London NODE_OPTIONS='$NODE_OPTIONS --experimental-vm-modules' jest --coverage=false --watch",
"test": "TZ=Europe/London vitest run",
"test:coverage": "TZ=Europe/London vitest run --coverage && open coverage/lcov-report/index.html",
"test:watch": "TZ=Europe/London vitest --ui --coverage",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

watch mode now throws up a nice UI like below

Screenshot from 2024-08-24 16-39-50

@github-actions
Copy link

github-actions bot commented Aug 24, 2024

Removed vultr server and associated DNS entries

Copy link
Contributor

@DafyddLlyr DafyddLlyr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic PR - thanks so much for the documentation links and explanations, super helpful.

"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:jest/recommended"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super happy for this to be a follow up PR - it might be worth looking at something like https://www.npmjs.com/package/eslint-plugin-vitest to replace this?

// Assert
expect(result).toHaveProperty(key);
layersToRollup.forEach((layer) => {
// Jest can handle paths using dot notation, so keys with a dot need to be wrapped in []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think "can" was correct here - the key here would have been something like a.b.c which Jest would have tried to default to a nested path, as opposed to the object in question which is likely { "a.b.c": "x" }

Either way, it's a good call to remove this comment - it looks like vitest handles this more gracefully - the test actually passes with or without the wrapping [] on line 154.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really appreciate the comments in this config file 💪

Comment on lines +16 to +17
functions: 55,
// TODO: could add autoUpdate flag here so that function coverage is only allowed to increase
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do this and leave this better than we found it 👍

@DafyddLlyr DafyddLlyr force-pushed the api-migrate-jest-vitest branch from a94d651 to c870585 Compare August 29, 2024 07:06
@DafyddLlyr
Copy link
Contributor

DafyddLlyr commented Aug 29, 2024

Merged in @freemvmt 's absence.

I'll pick up the two (very small) tasks suggested above in the comments -

@DafyddLlyr DafyddLlyr merged commit 3984823 into main Aug 29, 2024
@DafyddLlyr DafyddLlyr deleted the api-migrate-jest-vitest branch August 29, 2024 07:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants