nodejs / nodejs/node

experimental-test-module-mocks with ES imports does not reset mocked modules for tests in the same file

Open
#59,163 9 comments 8 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

never-stale test_runner
Dominant language
JavaScript
Stars
122k
Forks
37.3k
Avg merge
4d 2h
Merged PRs (30d)
283

Description

Version

24.4.1

Platform
Microsoft Windows NT 10.0.26100.0 x64
Subsystem

node:test

What steps will reproduce the bug?
// file1: utils/fs-wrapper.js
// Simple wrapper around fs.readFileSync to demonstrate the issue
import { readFileSync } from 'node:fs';

export default function readFile(path: string): string {
    return readFileSync(path, 'utf8');
}

// file2: utils/file-processor.js
// Module that depends on fs-wrapper
import readFile from './fs-wrapper.js';

export default function processFile(path: string): string {
    const content = readFile(path);
    return content.toUpperCase();
}

// file3: test/cache-issue.test.js
// Test demonstrating the ES module cache issue
import { test, describe, beforeEach, mock } from 'node:test';
import assert from 'node:assert/strict';

describe('ES Module Cache Issue with mock.restoreAll()', () => {
    beforeEach(() => {
        mock.restoreAll();
    });

    test('first test - should use mock', async (t) => {
        // Mock the fs-wrapper module
        const mockReadFile = mock.fn(() => 'mocked content from test 1');
        t.mock.module('../utils/fs-wrapper.js', {
            defaultExport: mockReadFile,
        });

        // Import the module that depends on fs-wrapper
        const fileProcessorModule = await import('../utils/file-processor.js');
        const processFile = fileProcessorModule.default;

        const result = processFile('dummy-path.txt');

        assert.strictEqual(result, 'MOCKED CONTENT FROM TEST 1');
        assert.strictEqual(mockReadFile.mock.callCount(), 1);
    });

    test('second test - should use different mock but fails due to cache', async (t) => {
        // Mock with different return value
        const mockReadFile = mock.fn(() => 'mocked content from test 2');
        t.mock.module('../utils/fs-wrapper.js', {
            defaultExport: mockReadFile,
        });

        // This import returns cached version that still references the old mock
        const fileProcessorModule = await import('../utils/file-processor.js');
        const processFile = fileProcessorModule.default;

        const result = processFile('dummy-path.txt');

        // This assertion will fail because the cached module still uses the old mock
        assert.strictEqual(result, 'MOCKED CONTENT FROM TEST 2');
        assert.strictEqual(mockReadFile.mock.callCount(), 1);
    });

    test('third test - workaround with cache busting', async (t) => {
        // Mock with third return value
        const mockReadFile = mock.fn(() => 'mocked content from test 3');
        t.mock.module('../utils/fs-wrapper.js', {
            defaultExport: mockReadFile,
        });

        // Use cache busting query parameter to force fresh import
        const fileProcessorModule = await import('../utils/file-processor.js?v=' + Date.now());
        const processFile = fileProcessorModule.default;

        const result = processFile('dummy-path.txt');

        // This assertion passes because we bypassed the cache
        assert.strictEqual(result, 'MOCKED CONTENT FROM TEST 3');
        assert.strictEqual(mockReadFile.mock.callCount(), 1);
    });
});

// Command line: node --test --experimental-test-module-mocks
How often does it reproduce? Is there a required condition?

Every time.

What is the expected behavior? Why is that the expected behavior?

The mocked module should reset between tests.

What do you see instead?
▶ ES Module Cache Issue with mock.restoreAll()
  ✔ first test - should use mock (43.5241ms)
  ✖ second test - should use different mock but fails due to cache (3.0297ms)
  ✔ third test - workaround with cache busting (4.2197ms)
✖ ES Module Cache Issue with mock.restoreAll() (52.1751ms)
ℹ tests 6
ℹ suites 1
ℹ pass 5
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 137.1057

✖ failing tests:

test at dist\tests\file-processor.test.js:22:5
✖ second test - should use different mock but fails due to cache (3.0297ms)
  AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
  actual expected

      at TestContext.<anonymous> (file:///C:/Users/xxx/Documents/GitHub/genesys-concepts/dist/tests/file-processor.test.js:34:16)
      at async Test.run (node:internal/test_runner/test:1069:7)
      at async Suite.processPendingSubtests (node:internal/test_runner/test:752:7) {
    generatedMessage: true,
    code: 'ERR_ASSERTION',
    actual: 'MOCKED CONTENT FROM TEST 1',
    expected: 'MOCKED CONTENT FROM TEST 2',
    operator: 'strictEqual'
  }
Additional information

Not sure if there is a better workaround for this or maybe I'm doing something wrong?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the reproduction in test/cache-issue.test.js with node --test --experimental-test-module-mocks. Read the node:test module-mocking behavior exercised by t.mock.module() and mock.restoreAll(), along with the file-processor.js and fs-wrapper.js imports. Done means the second test receives its own mock without the cache-busting query parameter, while the existing tests continue to pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
backend, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.