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

test: refactor fast suite to use vitest #3797

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open

test: refactor fast suite to use vitest #3797

wants to merge 40 commits into from

Conversation

erickzhao
Copy link
Member

@erickzhao erickzhao commented Jan 9, 2025

This PR moves our test:fast suite from Mocha/Chai/Proxyquire/Sinon to Vitest.

This migration was mostly a syntactical change. I tried to preserve the actual contents of the tests as much as possible.

File renames

By default, Vitest looks for Module.test.ts rather than Module_test.ts. This seems to be a common standard, so all files are renamed.

The new test suite is now under spec/ instead of test/ so that there's a clear delineation between the old Mocha tests and the newer Vitest tests.

All imports are now explicit

Mocha (like Jest) includes its functions (e.g. describe, it, before, etc.) as globals when executing tests.

On the other hand, Vitest requires you to explicitly import its test API functions.

+ import { describe, expect, it } from 'vitest';

Preferring Jest-like matchers over Chai-like matchers

Previously, this test suite used Mocha for running tests, which paired with Chai for assertions.

Vitest supports both Chai-like and Jest-like assertion syntaxes, which means that we technically didn't have to change any assertions. However, we use Jest across most of our other Electron ecosystem repos, so I migrated all tests to Jest-like syntax.

See comparison below:

// less function chaining
- expect(1+1).to.equal(2);
+ expect(1+1).toEqual(2);

// `toEqual` does deep object equality, while `toBe` compares references
- expect({foo: "bar"}).to.deep.equal({foo: "bar"});
+ expect({foo: "bar"}).toEqual({foo: "bar"});
+ expect({foo: "bar"}).not.toBe({foo: "bar"});

// `.not` is prepended in chain
- expect(1+3).to.not.equal(2);
+ expect(1+3).not.toEqual(2);

// comes with Promise handling out of the box
// (previously required `chai-as-promised` plugin package)
- await expect(Promise.resolve(1+1)).to.eventually.equal(2);
+ await expect(Promise.resolve(1+1)).resolves.toEqual(2);

- await expect(Promise.reject(1+1)).to.eventually.be.rejectedWith(2);
+ await expect(Promise.reject(1+1)).rejects.toThrow(2);

Use vi API for mocking

This test suite previously used a combination of libraries to mock external dependencies for unit tests:

  • proxyquire was used to mock The Node.js require module loader.
  • sinon was used to create function stubs.

Vitest comes with its own vi mocking API (note: it uses Sinon under the hood as well). It provides a more ergonomic approach than proxyquire (and supports ESM) and resembles jest.mock for those who are familiar with that API.

For example, take the mocking of imports in the start command tests:

    start = proxyquire.noCallThru().load('../../src/api/start', {
      '../util/resolve-dir': async (dir: string) => resolveStub(dir),
      '../util/read-package-json': {
        readMutatedPackageJson: () => Promise.resolve(packageJSON),
      },
      'node:child_process': {
        spawn: spawnStub,
      },
    }).default;
vi.mock(import('node:child_process'), async (importOriginal) => {
  const mod = await importOriginal();
  return {
    ...mod,
    spawn: vi.fn(),
  };
});

vi.mock(import('../../src/util/resolve-dir'), async () => {
  return {
    default: vi.fn().mockResolvedValue('dir'),
  };
});

vi.mock(import('@electron-forge/core-utils'), async (importOriginal) => {
  const mod = await importOriginal();
  return {
    ...mod,
    listrCompatibleRebuildHook: vi.fn(),
    getElectronVersion: vi.fn(),
  };
});
  • vi.fn() replaces the need to store sinon stub variables for each mocked function.
  • vi.mock now applies to each import directly rather than needing to rewire the function we're testing against (in this case, the start command).
  • vi.mock with a dynamic import is type-safe.
  • vi.mock provides the ability to only partially mock modules with the importOriginal helper function.

A note on mocking require

We use ES Module import syntax (i.e. import { foo } from 'bar') in most places in Forge's code. The TypeScript compiler will translate this into require calls under the hood because we output CommonJS in the build process. This is how proxyquire is still able to work despite us not using require directly.

vi.mock is easily able to work with this syntax as well, and also grants us the ability to mock pure ESM modules as well if the need arises. However, it does struggle with mocking dynamic require calls that we have in certain maker installers that we include in optionalDependencies.

For example:

async make({ dir, makeDir, targetArch }: MakerOptions): Promise<string[]> {
// eslint-disable-next-line node/no-missing-require
const installer = require('electron-installer-debian');

To properly mock require calls, I needed to add a new util to @electron-forge/test-utils that works around this issue with vi.hoisted:

// Helper function
export async function mockRequire(mockedUri: string, stub: any) {
  // eslint-disable-next-line node/no-unsupported-features/es-syntax
  const { Module } = await import('module');

  //@ts-expect-error undocumented functions
  Module._load_original = Module._load;
  //@ts-expect-error undocumented functions
  Module._load = (uri, parent) => {
    if (uri === mockedUri) return stub;
    //@ts-expect-error undocumented functions
    return Module._load_original(uri, parent);
  };
}
// Usage
vi.hoisted(async () => {
  const { mockRequire } = await import('@electron-forge/test-utils');
  void mockRequire('electron-installer-debian', vi.fn().mockResolvedValue({ packagePaths: ['/foo/bar.deb'] }));
});

msw over fetch-mock

Mock Service Worker (MSW) is the testing utility recommended by the Vitest documentation for mocking network requests.

It was a bit of a pain to get working because it doesn't provide an easy way to assert against calls (had to collect calls manually), but I made it happen.

@erickzhao erickzhao marked this pull request as ready for review January 9, 2025 21:11
@erickzhao erickzhao requested a review from a team as a code owner January 9, 2025 21:11
@erickzhao erickzhao changed the title test: refactor fast tests to use vitest test: refactor fast suite to use vitest Jan 9, 2025
@erickzhao erickzhao requested a review from a team January 9, 2025 21:18
.circleci/config.yml Outdated Show resolved Hide resolved
@@ -131,15 +126,14 @@
"minimist": "^1.2.6",
"mocha": "^9.0.1",
Copy link
Member Author

Choose a reason for hiding this comment

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

Note that Mocha and Chai are still needed for our slow test suite. I'll work on removing those in a follow-up PR.

return spawn('npx', args);
}

describe('cli', { timeout: 30_000 }, () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

how was 30s determined for this test? I'm wondering if we can revisit this and adjust as necessary

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 had the default and kept increasing it until it stopped flaking 😓

I think a future task would maybe be if we can move this to the slow test suite as well.

.circleci/config.yml Outdated Show resolved Hide resolved
Copy link
Member

@dsanders11 dsanders11 left a comment

Choose a reason for hiding this comment

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

Awesome! Excellent PR description, and thanks for trudging through the transition here.

Unfortunately GitHub didn't do a great job consolidating the diff for some of the renamed files, but not sure what caused it to work on some but not others. 🤔 I wonder if renaming both a parent directory and the filename makes it more likely to lose the connection?

@@ -0,0 +1,8 @@
/// <reference types="vitest/config" />
Copy link
Member

Choose a reason for hiding this comment

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

Is this necessary? I'd have expected the types to work correctly without it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I added it because it was recommended in the docs: https://vitest.dev/config/file.html#managing-vitest-config-file

IIRC it giving a type check error without it.

Copy link
Member

@BlackHole1 BlackHole1 left a comment

Choose a reason for hiding this comment

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

Code LGTM. This PR description is textbook-level!

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.

4 participants