npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@web/test-runner-module-mocking

v0.1.1

Published

Package to enable mocking modules in @web/test-runner

Downloads

4

Readme

Web Test Runner Module Mocking

Web Test Runner package that enables mocking of ES Modules during test runs.

Concept

A typical step when authoring tests is to change the behavior of dependencies of a system under test. This is usually needed to cover all branches of the system under test, for performance reasons or sometimes because a dependency cannot operate correctly in the test environment. Test authors will use mocks, stubs or spies to change or monitor the original implementation.

Dependencies that are on the global window object are easy to change. ES Modules, however, and specifically their exports bindings cannot be reassigned. This package exposes a Web Test Runner plugin that can intercept module imports on the server. When a module is intercepted, the plugins injects extra code that allows reassigning its exports.

Once the plugin is active in the Web Test Runner config, a test author can use the importMockable() function to start rewiring modules in test runs. The function imports the intercepted module and returns a mockable object. This objects contains all the exports of the module as its properties. Reassigning these properties allows rewiring the intercepted module, and the system under test will use the updated implementation.

import { importMockable } from '@web/test-runner-module-mocking';

const externalLibrary = await importMockable('external-library');

// Return the original function that 'external-library' exposed in the `externalFunction` named export
externalLibrary.externalFunction; // f externalFunction() { ... }

// Rewire the 'external-library' module to return this anonymous function as the `externalFunction` export
externalLibrary.externalFunction = () => console.log('hello world'); // () => console.log('hello world')

const { externalFunction } = await import('external-library');
externalFunction; // () => console.log('hello world')

Usage

Setup

// web-test-runner.config.mjs
import { moduleMockingPlugin } from '@web/test-runner-module-mocking/plugin.js';

export default {
  plugins: [moduleMockingPlugin()],
};

Simple test scenario

// src/getTimeOfDay.js
import { getCurrentHour } from 'time-library';

export function getTimeOfDay() {
  const hour = getCurrentHour();
  if (hour < 6 || hour > 18) {
    return 'night';
  }
  return 'day';
}
// test/getTimeOfDay.test.js
import { importMockable } from '@web/test-runner-module-mocking';

const timeLibrary = await importMockable('time-library');
const { getTimeOfDay } = await import('../src/getTimeOfDay.js');

describe('getTimeOfDay', () => {
    it('returns night at 2', () => {
        timeLibrary.getCurrentHour = () => 2;
        const result = getTimeOfDay();
        if (result !== 'night') {
            throw;
        }
    });
});

Extended test scenario

This scenario showcases how to use @web/test-runner-module-mocking together with chai and sinon.

// test/getTimeOfDay.test.js
import { stub } from 'sinon';
import { expect } from 'chai';
import { importMockable } from '@web/test-runner-module-mocking';

const timeLibrary = await importMockable('time-library');
const { getTimeOfDay } = await import('../src/getTimeOfDay.js');

describe('getTimeOfDay', () => {
  it('returns night at 2', () => {
    const stubGetCurrentHour = stub(timeLibrary, 'getCurrentHour').returns(2);
    try {
      const result = getTimeOfDay();
      expect(result).to.equal('night');
    } finally {
      stubGetCurrentHour.restore();
    }
  });
  it('returns day at 14', () => {
    const stubGetCurrentHour = stub(timeLibrary, 'getCurrentHour').returns(14);
    try {
      const result = getTimeOfDay();
      expect(result).to.equal('day');
    } finally {
      stubGetCurrentHour.restore();
    }
  });
});

Caveats

When designing the approach to allow modules to be mockable, a number of alternatives were considered:

  • Import Attributes
    eg. import externalLibrary from "external-library" with { type: "module-mockable" };
    This alternative was dismissed because Import Attributes is not yet widely implemented. This could be reconsidered in a future iteration.
  • Custom URL scheme
    eg. import externalLibrary from "module-mockable:external-library"
    This alternative was dismissed as the use of a unknown specifier was deemed magic.

In the end a combination of an async function (using an internal dynamic import()) and a top-level await was chosen. This choice, however, has a number of caveats.

Order of imports

In order to intercept a module, the module should not be referenced yet. (Once a module is loaded, the module loader also loads all its dependent modules) This means the module containing the system under test should be loaded after the module interception. As interception is achieved using a function, this also means the system under test should always be loaded using a dynamic import in order to be done after the interception.

import { importMockable } from '@web/test-runner-module-mocking';

// First, intercept
const externalLibrary = await importMockable('external-library');

// Second, load the system under test
const systemUnderTest = await import('../../src/system-under-test.js');

// Run tests
...

Types of module specifiers

Currently, two types of module specifiers are supported: bare modules and server relative modules.

// bare module, located in `node_modules`
await importMockable('external-library');

// server relative module
await importMockable('/src/library-to-intercept.js');

In order to use regular relative references, import.meta.resolve() and new URL().pathname can be used.

// relative module
await importMockable(new URL(import.meta.resolve('../src/library-to-intercept.js')).pathname);