esmock

repository·main·Indexed 42 days ago

https://github.com/iambumblehead/esmock

A utility for unit testing that provides native ESM (ECMAScript Modules) import and globals mocking. It allows developers to intercept module imports, package dependencies, and global variables like fetch or Date to create controlled testing environments. Features include strict and strictest modes for non-merging mocks, support for dynamic await import() via esmock.p, and cache management using esmock.purge. Version 2.7.6.

Tokens
3.8K
Snippets
14
Records
16
Agent score
53%

What's inside esmock

  1. Overview of esmock

    main
    esmock is a utility for unit testing that provides native ESM (ECMAScript Modules) import mocking and globals mocking. It allows you to intercept imports within a target module's dependency tree or mock global variables (like fetch or Date) to create controlled testing environments.
  2. Mock globals and the full import tree

    main

    The third argument to esmock allows you to mock objects that are used throughout the entire import tree. This is useful for mocking built-in modules like fs or global functions like fetch.

    import test from 'node:test'
    import assert from 'node:assert'
    import esmock from 'esmock'
    
    test('full import tree mocks —third param', async () => {
      const { getFile } = await esmock('../src/main.js', {}, {
        // mocks *every* fs.readFileSync inside the import tree
        fs: { readFileSync: () => 'returned to 🌲 every caller in the tree' }
      })
    
      assert.equal(getFile(), 'returned to 🌲 every caller in the tree')
    })
    
    test('mock fetch, Date, setTimeout and any globals', async () => {
      const { userCount } = await esmock('../Users.js', {
        '../req.js': await esmock('../req.js', {
          import: { // define globals like 'fetch' on the import namespace
            fetch: async () => ({
              status: 200,
              json: async () => [['jim','😄'],['jen','😊']]
            })
          }
        })
      })
    
      assert.equal(await userCount(), 2)
    })
  3. Mock package, alias, and local file imports

    main

    You can mock specific dependencies by providing their name or path in the second argument of esmock. This includes standard npm packages, subpath imports (aliases like #icon), and relative file paths.

    import test from 'node:test'
    import assert from 'node:assert'
    import esmock from 'esmock'
    
    test('package, alias and local file mocks', async () => {
      const cookup = await esmock('../src/cookup.js', {
        addpkg: (a, b) => a + b,
        '#icon': { coffee: '☕', bacon: '🥓' },
        '../src/breakfast.js': {
          default: () => ['coffee', 'bacon'],
          addSalt: meal => meal + '🧂'
        }
      })
    
      assert.equal(cookup('breakfast'), '☕🥓🧂')
    })
  4. Use esmock.p to mock 'await import()' calls

    main

    When your code uses dynamic await import(), use esmock.p. This version of the function ensures that mock definitions are kept in a cache, so when the dynamic import is called, the mocked definition is returned correctly.

    import test from 'node:test'
    import assert from 'node:assert'
    import esmock from 'esmock'
    
    test('mocks "await import()" using esmock.p', async () => {
      const doAwaitImport = await esmock.p('../awaitImportLint.js', {
        eslint: { ESLint: cfg => cfg }
      })
    
      assert.equal(await doAwaitImport('cfg🛠️'), 'cfg🛠️')
    })
  5. Use esmock() to mock imports and globals

    main

    The primary esmock function accepts three arguments:

    1. path: The path to the target module being tested.
    2. childmocks: An object defining mocks for modules imported by the target module (supports package names, aliases, and local file paths).
    3. globalmocks: An object defining mocks that are applied to the global scope or the import namespace, affecting the entire import tree.

    Note: For Node.js versions prior to v20.6.0, you must use the --loader command line argument with esmock. Current versions of Node.js do not require this.

    await esmock(
      './to/module.js', // path to target module being tested
      { ...childmocks }, // mock definitions imported by target module
      { ...globalmocks }) // mock definitions imported everywhere
  6. Use esmock.strict for non-merging mocks

    main

    By default, esmock may merge mocks with existing module definitions. If you want to completely replace the original module definitions without merging them, use esmock.strict. If your mock object is missing properties that the original module had, accessing those properties will result in a TypeError.

    import test from 'node:test'
    import assert from 'node:assert'
    import esmock from 'esmock'
    
    test('esmock.strict mocks', async () => {
      // replace original module definitions and do not merge them
      const pathWrapper = await esmock.strict('../src/pathWrapper.js', {
        path: { dirname: () => '/path/to/file' }
      })
    
      // error, because "path" mock above does not define path.basename
      await assert.rejects(() => pathWrapper.basename('/dog.🐶.png'), {
        name: 'TypeError',
        message: 'path.basename is not a function'
      })
    })
  7. ESLint configuration for esmock

    main

    The esmock repository uses a flat configuration for ESLint. It integrates @eslint/js, @eslint/markdown, and typescript-eslint to enforce code quality across JavaScript, TypeScript, and Markdown files. Key features include:

    • Markdown Support: Uses @eslint/markdown to process code blocks within .md files.
    • TypeScript Integration: Applies typescript-eslint recommended rules with specific overrides to allow any types and unused expressions/variables in certain contexts.
    • Global Variables: Defines globals for testing environments (e.g., describe, it, test, expect) and specific esmock utilities (esmock, childmocks, globalmocks) within Markdown code blocks.
    • Code Style: Enforces a specific style including 2-space indentation, no semicolons, and specific rules for variable declarations and arrow functions.
  8. Purge mocked modules from the cache

    main

    When using esmock, modules are cached to optimize performance. If you need to re-mock a module or ensure a fresh import, you can manually purge it using the purge function.

    To use purge, you must pass the imported module object returned by esmock. The function checks if the object is a valid mockable entity (object or function) and contains the required esmkTreeId.

    import esmock, { purge } from 'esmock';
    
    const mod = await esmock('./module.js', import.meta.url, { './dep.js': {} });
    
    // ... perform tests ...
    
    // Clear the module from the esmock cache
    purge(mod);
  9. Use esmock.strict to replace original module definitions

    main

    The esmock.strict variant (and the standalone strict export) replaces original module definitions with your provided mock definitions instead of merging them. This is useful when you want to ensure the original implementation is not executed at all.

    import { strict } from 'esmock';
    
    const mockedModule = await strict('./my-module.js', import.meta.url, {
      './dependency.js': { mockedValue: true }
    });
  10. Use esmock strict modes for enhanced validation

    main

    The esmock package exports specialized versions of the main function to control strictness levels. These are useful when you need more rigorous enforcement of module boundaries or dependency resolution.

    • esmock: The standard version with default strictness.
    • strict: An alias for esmock with strict: 1 enabled.
    • strictest: An alias for esmock with strict: 3 enabled.

    Each of these also provides a .p() method, which is the same function but with purge: false applied, preventing the automatic purging of the module from the internal cache.

    import { strict, strictest } from 'esmock';
    
    // Uses strict mode (level 1)
    const mod = await strict('./module.js', import.meta.url, { mock: {} });
    
    // Uses highest strictness (level 3)
    const modStrict = await strictest('./module.js', import.meta.url, { mock: {} });
    
    // Use .p() to prevent automatic purging of the module cache
    const modNoPurge = await strict.p('./module.js', import.meta.url, { mock: {} });
  11. Use esmock to mock ESM imports and globals

    main

    The esmock function is the primary entrypoint for mocking ECMAScript Modules (ESM) and global definitions. It allows you to import a module while providing custom definitions (defs) for its dependencies and custom global definitions (gdefs).

    By default, esmock also manages a cache (purging) to ensure that subsequent calls to the same module reflect new mocks. You can control this behavior using the purge option.

    Usage Pattern: await esmock(moduleId, parent, defs, gdefs, options)

    • moduleId: The string path or URL of the module to import.
    • parent: The module or context requesting the import.
    • defs: An object containing the mocks for specific module imports.
    • gdefs: An object containing mocks for global variables.
    • options: Configuration object (e.g., { purge: false }).
    import esmock from 'esmock';
    
    const myMock = { method: () => 'mocked' };
    const module = await esmock('./target-module.js', import.meta.url, { './dependency.js': myMock });
  12. Use esmock to mock module imports

    main

    The esmock function allows you to mock imports for a specific module. You provide a modulePath (the module to be mocked), a parent (a URL used to resolve relative specifiers, typically import.meta.url), and defs (a mapping of import specifiers to mock definitions).

    By default, esmock merges your mock definitions with the original module definitions. To completely replace the original definitions, use the strict or strictest variants.

    import esmock from 'esmock';
    
    // Basic usage: mocking a module
    const mockedModule = await esmock('./my-module.js', import.meta.url, {
      './dependency.js': { mockedValue: true }
    });