Related
I've tried to search for answer to this problem for some time now and I failed. So I decided to give it a try here. If there is a such question already and I missed it, I'm sorry for duplicate.
Assume I have this javascript module 'myValidator.js', where are two functions and one function calls the other one.
export const validate = (value) => {
if (!value.something) {
return false
}
// other tests like that
return true
}
export const processValue = (value) => {
if (!validate(value)) {
return null
}
// do some stuff with value and return something
}
Test for this like this.
I want to test the validate function, whether is behaves correctly. And then I have the processValue function that invokes the first one and returns some value when validation is ok or null.
import * as myValidator from './myValidator'
describe('myValidator', () => {
describe('validate', () => {
it('should return false when something not defined', () => {
...
}
}
describe('processValue', () => {
it('should return something when value is valid', () => {
const validateMock = jest.spyOn(myValidator, 'validate')
validateMock.mockImplementation(() => true)
expect(validate('something')).toEqual('somethingProcessed')
}
it('should return null when validation fails', () => {
const validateMock = jest.spyOn(myValidator, 'validate')
validateMock.mockImplementation(() => false)
expect(validate('somethingElse')).toEqual(null)
}
}
}
Now, the problem is that this doesn't actually work as processValue() actually calls the function inside the module, because of the closure I suppose. So the function is not mocked as only the reference in exports is changed to jest mock, I guess.
I have found a solution for this and inside the module to use
if (!exports.validate(value))
That works for the tests. However we use Webpack (v4) to build the app, so it transforms those exports into its own structure and then when the application is started, the exports is not defined and the code fails.
What's best solution to test this?
Sure I can do it with providing some valid and invalid value, for this simple case that would work, however I believe it should be tested separately.
Or is it better to not mock functions and call it through to avoid the problem I have or is there some way how to achieve this with JavaScript modules?
I finally found the answer to this question. It's actually in the the Jest examples project on GitHub.
// Copyright 2004-present Facebook. All Rights Reserved.
/**
* This file illustrates how to do a partial mock where a subset
* of a module's exports have been mocked and the rest
* keep their actual implementation.
*/
import defaultExport, {apple, strawberry} from '../fruit';
jest.mock('../fruit', () => {
const originalModule = jest.requireActual('../fruit');
const mockedModule = jest.genMockFromModule('../fruit');
//Mock the default export and named export 'apple'.
return {
...mockedModule,
...originalModule,
apple: 'mocked apple',
default: jest.fn(() => 'mocked fruit'),
};
});
it('does a partial mock', () => {
const defaultExportResult = defaultExport();
expect(defaultExportResult).toBe('mocked fruit');
expect(defaultExport).toHaveBeenCalled();
expect(apple).toBe('mocked apple');
expect(strawberry()).toBe('strawberry');
});
I'm writing a Jest mock, but I seem to have a problem when defining a mocked function outside of the mock itself.
I have a class:
myClass.js
class MyClass {
constructor(name) {
this.name = name;
}
methodOne(val) {
return val + 1;
}
methodTwo() {
return 2;
}
}
export default MyClass;
And a file using it:
testSubject.js
import MyClass from './myClass';
const classInstance = new MyClass('Fido');
const testSubject = () => classInstance.methodOne(1) + classInstance.name;
export default testSubject;
And the test:
testSubject.test.js
import testSubject from './testSubject';
const mockFunction = jest.fn(() => 2)
jest.mock('./myClass', () => () => ({
name: 'Name',
methodOne: mockFunction,
methodTwo: jest.fn(),
}))
describe('MyClass tests', () => {
it('test one', () => {
const result = testSubject()
expect(result).toEqual('2Name')
})
})
However, I get the following error:
TypeError: classInstance.methodOne is not a function
If I instead write:
...
methodOne: jest.fn(() => 2)
Then the test passes no problem.
Is there a way of defining this outside of the mock itself?
In my case, I had to mock a Node.js module. I'm using React and Redux in ES6, with Jest and Enzyme for unit tests.
In the file I'm using, and writing a test for, I'm importing the node modules as default:
import nodeModulePackage from 'nodeModulePackage';
So I needed to mock it as a default since I kept getting the error (0, _blah.default) is not a function..
My solution was to do:
jest.mock('nodeModulePackage', () => jest.fn(() => {}));
In my case, I just needed to override the function and make it return an empty object.
If you need to call a function on that node module, you'll do the following:
jest.mock('nodeModulePackage', () => ({ doSomething: jest.fn(() => 'foo') }));
I figured this out. It is to do with hoisting, see: Jest mocking reference error
The reason it had worked in a previous test, where I had done it, was because the testSubject was itself a class. This meant that when the testSubject was instantiated, it was after the variable declaration in the test file, so the mock had access to use it.
So in the above case it was never going to work.
Defining mockOne as an unassigned let and then initialising the variable inside the mocking function worked for me:
let mockFunction
jest.mock('./myClass', () => () => {
mockFunction = jest.fn(() => 2)
return {
name: 'Name',
methodOne: mockFunction,
methodTwo: jest.fn(),
}
}))
In my case, it was the importing, as described at Getting `TypeError: jest.fn is not a function` (posting here because my problem showed in jest.mock, so I looked here first).
The basic problem was that someone had put in
const jest = require('jest');
But that is wrong (should be jest-mock instead of jest). This had worked in Node.js 10 with the line as originally put. It did not work in Node.js 14. I'm guessing that they used different versions of Jest, perhaps indirectly (other packages needed updated for 14).
const jest = require('jest-mock');
Also, in a test file, it will probably already be done for you, so you don't need to require anything. The file will run properly without that line.
Linting may give an error with that line omitted, but that can be fixed with
/* global jest */
or if you're already doing that with expect,
/* global expect, jest */
The linter seemed happy when that line appeared either after or before jest was first used. You may choose order based on readability rather than by functional requirement if your linter works the same way.
In my case it was from the module.exports.
Instead of writing
module.exports = {sum: sum};
Write
module.exports = sum;
Note: sum is a function that adds 2 numbers
In case anyone is still facing a similar issue, to resolve the failing tests I had to return a mocked function like so:
const tea = TeamMaker.makeTea();
tea(); // TypeError: tea is not a function
jest.mock('url/to/TeamMaker', () => ({ makeTea: jest.fn(() => jest.fn) })); // tests passed
For the other Jest newbies out there, if you mock multiple functions from the same package or module like this:
jest.mock(
'pathToModule',
() => ({
functionA: jest.fn(() => {
return 'contentA';
}),
})
);
jest.mock(
'pathToModule',
() => ({
functionB: jest.fn(() => {
return 'contentB';
}),
})
);
The second mock will override the first, and you'll end up with functionA is not a function.
Just combine them if they're both coming from the same module.
jest.mock(
'samePathToModule',
() => ({
functionA: jest.fn(() => {
return 'contentA';
}),
functionB: jest.fn(() => {
return 'contentB';
}),
})
);
For me, it was the jest config.
Because my project was initially in .js and was upgrade to .ts.
So the class I tested was in .ts but my jest.config.js was config for .jsfile.
See below my new config :
module.exports = {
...
collectCoverageFrom: ['src/**/*.{ts,js,jsx,mjs}'],
...
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{ts,js,jsx,mjs}',
'<rootDir>/src/**/?(*.)(spec|test).{ts,js,jsx,mjs}',
]
};
In my case, it was the way i was exporting the files that was the issue.
The file i was trying to mock was being exported like:
export const fn = () => {};
The mocked file i wrote was being exported like:
const fn = () => {};
export default fn;
Once i made sure that the mocked file was also being exported like the file to be mocked, i didn't have this issue
following is the answer which worked for me
import {testSubject} from '../testsubject';
describe('sometest',(){
it('some test scenario',()=>{
testSubject.mockReturnValue(()=>jest.fn);
})
})
I am writing my version in case it helps someone.
I was facing error while mocking firebase-admin.
I jest-mocked firebase-admin like below:
jest.mock('firebase-admin');
But I started getting following error:
firebase_admin_1.default.messaging is not a function
It was coming because in the app code, I had used firebase-admin like this:
await admin.messaging().send(message)
(message is an instance of TokenMessage)
The problem was that I was not mocking the member variables and member methods. Here it was the member method messaging and a nested member method within messaging called send. (See that messaging method is called on admin and then send is called on the value of admin.messaging()). All that was needed was to mock these like below:
jest.mock('firebase-admin', () => ({
credential: {
cert: jest.fn(),
},
initializeApp: jest.fn(),
messaging: jest.fn().mockImplementation(() => {
return {
send: jest
.fn()
.mockReturnValue('projects/name_of_project/messages/message_id'),
};
}),
}));
(Note that I have also mocked other member variables/methods as per my original requirement. You would probably need these mocks as well)
IT HAS TO BE EXPORTED.
like node function.
e.g. helper.js has myLog function
at the end,
module.exports = {
sleep,
myLog
};
now spec file can import and call them.
No tsx or jest.config.js changes...
We are porting a very large project into Vue.js and would like to thoroughly implement our unit testing suite as we go.
We would like to validate that components are having all their required properties set at creation, my test case looks something like:
describe('Input', () => {
it('fail initialization with no value', () => {
const vm = mount(Input, {
propsData: {},
});
// expecting above to fail due to required prop 'value'
});
});
The component Input is expected to contain a property value. I can see via the console warning emitted that it is missing but we would like to catch this in our unit tests. Directly testing this doesn't make much sense but once we have components nesting several components, making sure that each component correctly initialises its sub components is important and this is achieved by assuring ourselves that the above test should fail and be caught failing.
There is no exception raised however, we have had ideas to hook into the Vue.warnHandler but this just seems like a lot of work for simply confirming that a constructor works as expected.
Is there a recommended approach to doing this with Vue.js?
Unfortunately it looks like the only way to do this is to hook into the Vue.warnHandler
I used a variable and reset it on the afterEach hook (Mocha) like so:
let localVue;
let hasWarning = false;
function functionToWrapShallowMount(propsData) {
const Wrapper = shallowMount(Control, {
propsData,
provide: {
$validator: () => {}
},
localVue
});
return Wrapper;
}
before(() => {
localVue = createLocalVue();
Vue.config.warnHandler = () => {
hasWarning = true;
};
});
afterEach(() => {
hasWarning = false;
});
it('throws a warning', () => {
const { vm } = functionToWrapShallowMount({
name: 'foooo',
value: 'bad'
});
vm.name.should.eq('foooo');
hasWarning.should.eq(true);
});
The catch to this is you cannot use the localVue from Vue test utils you must use the Vue instance imported from vue.
I find Karma js tests somewhat cumbersome to set up and write and find myself often ignoring writing tests because of this, so I wanted to know if there exist better alternatives.
Since I use typescript my dream scenario would be if I could write something like this:
module adder {
export function add(a, b){
return a + b;
}
}
[Tests]
assert.equal(4, adder.add(2, 2));
My tests are inline and would be run directly in my editor when changes in current file occur. Since typescript could easily remove tests from final output I could put my tests in the same file as my code (The closer the better in my opinion). Does any testing framework support this and if not what would be needed to support this scenario.
Just a pedantic note - Karma is a test runner, not a test framework. However, it can use Jasmine, Mocha, QUnit or roll-your-own test framework.
You might be able to use decorator syntax to accomplish this type of behaviour. Something like:
#TestSuite
class AdderTests {
#Test
test1() {
assert.equal(4, adder.add(2,2));
}
#Test
test2() {
assert.equal(0, adder.add(2,-2));
}
}
Having said this, though, the structure of the test code is very similar to Jasmine syntax:
describe('AdderTests', () => {
it('should add 2 and 2', () => {
expect(adder.add(2,2)).toBe(4);
}
});
Your ideal, i.e:
[Test]
assert.equal( ... )
Is not really possible using TypeScript decorators, or even generics. You are trying to apply an attribute to an arbitrary line of code. This would have to be a function for a start, in order to correctly use JavaScript scoping rules:
[Test]
test1() { assert.equal( ... ) };
I suppose that any framework which declares tests as ordinary functions or objects can be made to support inline tests. You could declare the tests in Jasmine syntax as in blorkfish's answer alongside the code being tested. However I don't think a framework is needed.
It's worth remembering that the top level statements in a Type/JavaScript file are executed when it's imported (unless it is a type only import). So you can add test functions or objects to a global list of tests. Once all modules have been imported you can then run the tests.
For example let's say we have lib/test.ts
interface Suites {
[index: string]: Tests;
};
interface Tests {
[index: string]: () => Promise<void> | void;
};
let suites: Suites = {};
export function suite(name: string)
{
const suite: Tests = {};
suites[name] = suite;
return suite;
}
export async function run(on_pass: (desc: string) => void,
on_fail: (desc: string, error: Error) => void)
{
for (let suite in suites) {
for (let test in suites[suite]) {
const desc = `${suite}: ${test}`;
try {
// this will run the tests in serial
await suites[suite][test]();
on_pass(desc);
} catch (e) {
on_fail(desc, e as Error);
}
}
}
}
Then we can define a test like so
import { strict as assert } from 'node:assert';
import { inspect } from 'node:util';
import { suite } from 'lib/test';
// Create a test suite/group
const test = suite('lib/google');
// Not important
export type DateTime = { dateTime: string, };
export type DateOnly = { date: string, };
export function json2date(dt: DateTime | DateOnly)
{
// left as an exercise to the reader
}
// Add the test to the suite
test['json2date'] = () => {
const dt = {
dateTime: '2023-01-02T14:42:25.861Z',
};
const d = {
date: '2026-01-01',
};
assert.deepEqual(json2date(dt), new Date(dt.dateTime));
assert.deepEqual(json2date(d), new Date(2026, 0, 1));
};
test['An async test'] = async () => {
// test some async code
};
Then we need to load and execute all the tests. I do this as part of the apps health checks. That is, when a GET request is made to /sys/health, some code like the following is run
import { ... } from 'lib/google';
import { run as run_tests } from 'lib/test';
...
app.get('/sys/health', async () => {
let passes: number = 0;
let fails: number = 0;
const on_pass = (desc: string) => {
passes++;
log.info(`Test PASS: ${desc}`);
};
const on_fail = (desc: string, err: Error) => {
fails++;
log.error(`Test FAIL: ${desc}: ${inspect(err)}`);
};
await run_tests(on_pass, on_fail);
log.info(`Test summary: ${passes} passes, ${fails} fails, ${passes + fails} in total`);
if (fails > 0)
// return 500 to cause health checks to fail
});
Similar code can be used on the client. Indeed if a user experiences a client side error you can automatically run the client side tests in the background of the error page. The tests that will get run will depend on what modules were included.
The downside to this is that the test code will get included alongside the ordinary code. This isn't an issue if you run your unit tests in production as health checks. Also most bundlers support 'defines' and dead code elimination, so you can remove them from production code (e.g. https://esbuild.github.io/api/#define).
I have the following ES6 modules:
File network.js
export function getDataFromServer() {
return ...
}
File widget.js
import { getDataFromServer } from 'network.js';
export class Widget() {
constructor() {
getDataFromServer("dataForWidget")
.then(data => this.render(data));
}
render() {
...
}
}
I'm looking for a way to test Widget with a mock instance of getDataFromServer. If I used separate <script>s instead of ES6 modules, like in Karma, I could write my test like:
describe("widget", function() {
it("should do stuff", function() {
let getDataFromServer = spyOn(window, "getDataFromServer").andReturn("mockData")
let widget = new Widget();
expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
expect(otherStuff).toHaveHappened();
});
});
However, if I'm testing ES6 modules individually outside of a browser (like with Mocha + Babel), I would write something like:
import { Widget } from 'widget.js';
describe("widget", function() {
it("should do stuff", function() {
let getDataFromServer = spyOn(?????) // How to mock?
.andReturn("mockData")
let widget = new Widget();
expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
expect(otherStuff).toHaveHappened();
});
});
Okay, but now getDataFromServer is not available in window (well, there's no window at all), and I don't know a way to inject stuff directly into widget.js's own scope.
So where do I go from here?
Is there a way to access the scope of widget.js, or at least replace its imports with my own code?
If not, how can I make Widget testable?
Stuff I considered:
a. Manual dependency injection.
Remove all imports from widget.js and expect the caller to provide the deps.
export class Widget() {
constructor(deps) {
deps.getDataFromServer("dataForWidget")
.then(data => this.render(data));
}
}
I'm very uncomfortable with messing up Widget's public interface like this and exposing implementation details. No go.
b. Expose the imports to allow mocking them.
Something like:
import { getDataFromServer } from 'network.js';
export let deps = {
getDataFromServer
};
export class Widget() {
constructor() {
deps.getDataFromServer("dataForWidget")
.then(data => this.render(data));
}
}
then:
import { Widget, deps } from 'widget.js';
describe("widget", function() {
it("should do stuff", function() {
let getDataFromServer = spyOn(deps.getDataFromServer) // !
.andReturn("mockData");
let widget = new Widget();
expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
expect(otherStuff).toHaveHappened();
});
});
This is less invasive, but it requires me to write a lot of boilerplate for each module, and there's still a risk of me using getDataFromServer instead of deps.getDataFromServer all the time. I'm uneasy about it, but that's my best idea so far.
I've started employing the import * as obj style within my tests, which imports all exports from a module as properties of an object which can then be mocked. I find this to be a lot cleaner than using something like rewire or proxyquire or any similar technique. I've done this most often when needing to mock Redux actions, for example. Here's what I might use for your example above:
import * as network from 'network.js';
describe("widget", function() {
it("should do stuff", function() {
let getDataFromServer = spyOn(network, "getDataFromServer").andReturn("mockData")
let widget = new Widget();
expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
expect(otherStuff).toHaveHappened();
});
});
If your function happens to be a default export, then import * as network from './network' would produce {default: getDataFromServer} and you can mock network.default.
Note: the ES spec defines modules as read-only, and many ES transpilers have started honoring this, which may break this style of spying. This is highly dependent on your transpiler as well as your test framework. For example, I think Jest performs some magic to make this work, though Jasmine does not, at least currently. YMMV.
carpeliam is correct, but note that if you want to spy on a function in a module and use another function in that module calling that function, you need to call that function as part of the exports namespace, otherwise the spy won't be used.
Wrong example:
// File mymodule.js
export function myfunc2() {return 2;}
export function myfunc1() {return myfunc2();}
// File tests.js
import * as mymodule
describe('tests', () => {
beforeEach(() => {
spyOn(mymodule, 'myfunc2').and.returnValue = 3;
});
it('calls myfunc2', () => {
let out = mymodule.myfunc1();
// 'out' will still be 2
});
});
Right example:
export function myfunc2() {return 2;}
export function myfunc1() {return exports.myfunc2();}
// File tests.js
import * as mymodule
describe('tests', () => {
beforeEach(() => {
spyOn(mymodule, 'myfunc2').and.returnValue = 3;
});
it('calls myfunc2', () => {
let out = mymodule.myfunc1();
// 'out' will be 3, which is what you expect
});
});
vdloo's answer got me headed in the right direction, but using both CommonJS "exports" and ES6 module "export" keywords together in the same file did not work for me (Webpack v2 or later complains).
Instead, I'm using a default (named variable) export wrapping all of the individual named module exports and then importing the default export in my tests file. I'm using the following export setup with Mocha/Sinon and stubbing works fine without needing rewire, etc.:
// MyModule.js
let MyModule;
export function myfunc2() { return 2; }
export function myfunc1() { return MyModule.myfunc2(); }
export default MyModule = {
myfunc1,
myfunc2
}
// tests.js
import MyModule from './MyModule'
describe('MyModule', () => {
const sandbox = sinon.sandbox.create();
beforeEach(() => {
sandbox.stub(MyModule, 'myfunc2').returns(4);
});
afterEach(() => {
sandbox.restore();
});
it('myfunc1 is a proxy for myfunc2', () => {
expect(MyModule.myfunc1()).to.eql(4);
});
});
I implemented a library that attempts to solve the issue of run time mocking of TypeScript class imports without needing the original class to know about any explicit dependency injection.
The library uses the import * as syntax and then replaces the original exported object with a stub class. It retains type safety so your tests will break at compile time if a method name has been updated without updating the corresponding test.
This library can be found here: ts-mock-imports.
I have found this syntax to be working:
My module:
// File mymod.js
import shortid from 'shortid';
const myfunc = () => shortid();
export default myfunc;
My module's test code:
// File mymod.test.js
import myfunc from './mymod';
import shortid from 'shortid';
jest.mock('shortid');
describe('mocks shortid', () => {
it('works', () => {
shortid.mockImplementation(() => 1);
expect(myfunc()).toEqual(1);
});
});
See the documentation.
I haven't tried it myself, but I think mockery might work. It allows you to substitute the real module with a mock that you have provided. Below is an example to give you an idea of how it works:
mockery.enable();
var networkMock = {
getDataFromServer: function () { /* your mock code */ }
};
mockery.registerMock('network.js', networkMock);
import { Widget } from 'widget.js';
// This widget will have imported the `networkMock` instead of the real 'network.js'
mockery.deregisterMock('network.js');
mockery.disable();
It seems like mockery isn't maintained anymore and I think it only works with Node.js, but nonetheless, it's a neat solution for mocking modules that are otherwise hard to mock.
I recently discovered babel-plugin-mockable-imports which handles this problem neatly, IMHO. If you are already using Babel, it's worth looking into.
See suppose I'd like to mock results returned from isDevMode() function in order to check to see how code would behave under certain circumstances.
The following example is tested against the following setup
"#angular/core": "~9.1.3",
"karma": "~5.1.0",
"karma-jasmine": "~3.3.1",
Here is an example of a simple test case scenario
import * as coreLobrary from '#angular/core';
import { urlBuilder } from '#app/util';
const isDevMode = jasmine.createSpy().and.returnValue(true);
Object.defineProperty(coreLibrary, 'isDevMode', {
value: isDevMode
});
describe('url builder', () => {
it('should build url for prod', () => {
isDevMode.and.returnValue(false);
expect(urlBuilder.build('/api/users').toBe('https://api.acme.enterprise.com/users');
});
it('should build url for dev', () => {
isDevMode.and.returnValue(true);
expect(urlBuilder.build('/api/users').toBe('localhost:3000/api/users');
});
});
Exemplified contents of src/app/util/url-builder.ts
import { isDevMode } from '#angular/core';
import { environment } from '#root/environments';
export function urlBuilder(urlPath: string): string {
const base = isDevMode() ? environment.API_PROD_URI ? environment.API_LOCAL_URI;
return new URL(urlPath, base).toJSON();
}
You can use putout-based library mock-import for this purpose.
Let's suppose you have a code you want to test, let it be cat.js:
import {readFile} from 'fs/promises';
export default function cat() {
const readme = await readFile('./README.md', 'utf8');
return readme;
};
And tap-based test with a name test.js will look this way:
import {test, stub} from 'supertape';
import {createImport} from 'mock-import';
const {mockImport, reImport, stopAll} = createMockImport(import.meta.url);
// check that stub called
test('cat: should call readFile', async (t) => {
const readFile = stub();
mockImport('fs/promises', {
readFile,
});
const cat = await reImport('./cat.js');
await cat();
stopAll();
t.calledWith(readFile, ['./README.md', 'utf8']);
t.end();
});
// mock result of a stub
test('cat: should return readFile result', async (t) => {
const readFile = stub().returns('hello');
mockImport('fs/promises', {
readFile,
});
const cat = await reImport('./cat.js');
const result = await cat();
stopAll();
t.equal(result, 'hello');
t.end();
});
To run test we should add --loader parameter:
node --loader mock-import test.js
Or use NODE_OPTIONS:
NODE_OPTIONS="--loader mock-import" node test.js
On the bottom level mock-import uses transformSource hook, which replaces on the fly all imports with constants declaration to such form:
const {readFile} = global.__mockImportCache.get('fs/promises');
So mockImport adds new entry into Map and stopAll clears all mocks, so tests not overlap.
All this stuff needed because ESM has it's own separate cache and userland code has no direct access to it.
Here is an example to mock an imported function
File network.js
export function exportedFunc(data) {
//..
}
File widget.js
import { exportedFunc } from 'network.js';
export class Widget() {
constructor() {
exportedFunc("data")
}
}
Test file
import { Widget } from 'widget.js';
import { exportedFunc } from 'network'
jest.mock('network', () => ({
exportedFunc: jest.fn(),
}))
describe("widget", function() {
it("should do stuff", function() {
let widget = new Widget();
expect(exportedFunc).toHaveBeenCalled();
});
});
I haven't been able to try it out yet, but (Live demo at codesandbox.io/s/adoring-orla-wqs3zl?file=/index.js)
If you have a browser-based test runner in theory you should be able to include a Service Worker that can intercept the request for the ES6 module you want to mock out and replace it with an alternative implementation (similar or the same as how Mock Service Worker approaches things)
So something like this in your service worker
self.addEventListener('fetch', (event) => {
if (event.request.url.includes("canvas-confetti")) {
event.respondWith(
new Response('const confetti=function() {}; export default confetti;', {
headers: { 'Content-Type': 'text/javascript' }
})
);
}
});
If your source code is pulling in an ES6 module like this
import confetti from 'https://cdn.skypack.dev/canvas-confetti';
confetti();