Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Aug 29, 2024

Conversation

freemvmt
Copy link
Contributor

@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
Contributor 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
Contributor 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
Contributor 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
Contributor 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.

@@ -23,7 +23,9 @@ jest.mock("@opensystemslab/planx-core", () => {
});

describe("CSV data admin endpoint", () => {
afterEach(() => jest.clearAllMocks());
afterEach(() => {
Copy link
Contributor 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

@@ -18,7 +18,7 @@ it("returns an error if required query param is missing", async () => {
// "Success" test commented out due to reliance on external API calls and fallibility of nocks
// 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
Contributor Author

Choose a reason for hiding this comment

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

See relevant docs

@@ -151,7 +151,6 @@ describe("rollupResultLayer helper function", () => {
// 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 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.

@@ -3,9 +3,10 @@ import app from "../../../../server.js";
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
Contributor 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

@@ -12,14 +12,16 @@ const invalidBody = {
wrong: "message",
};

const mockSend = jest.fn();
jest.mock<typeof SlackNotify>("slack-notify", () =>
Copy link
Contributor 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
Contributor 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

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.

"extends": [
"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?

@@ -151,7 +151,6 @@ describe("rollupResultLayer helper function", () => {
// 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
12 checks passed
@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