Skip to content

Commit

Permalink
Support immediately require calls
Browse files Browse the repository at this point in the history
Add support to modules that instant executing a required module.

    require('./something')()

This mock-fn-as-callback-style was used in test found in npm/cli:

    t.test('mock as callback style', t => {
      t.mock('../my-module.js', {
        '../sub-module.js': arg => {
          t.equal(arg, 'expected')
          t.end()
        }
      })
    })

Also added more varied situations to stress test the module replacement,
such as using t.mock within one of the defined mocks, require calls at
execution time along with tests for the mock as callback style.
  • Loading branch information
ruyadorno committed Nov 12, 2020
1 parent d7e3007 commit 51f45f5
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 37 deletions.
48 changes: 31 additions & 17 deletions lib/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,52 @@ class Mock {
are the same used in ${filename} require calls`)
}

let reload = false
const self = this
const callerTestRef = Module._cache[parentFilename]
const filePath = Module._resolveFilename(filename, callerTestRef)
if (!Module._cache[filePath]) {
Module._load(filename, callerTestRef, false)
}

// populate mocks Map from resolved filenames
for (const key of Object.keys(mocks)) {
const mockFilePath = Module._resolveFilename(key, callerTestRef)
this.mocks.set(mockFilePath, mocks[key])
}

this.original = Module._cache[filePath]
const originalRequire = this.original.require
const __require = Module.prototype.require
function tapRequireMock (id) {
if (this.filename === filePath) {
const requiredFilePath = Module._resolveFilename(id, this)
const res = self.mocks.get(requiredFilePath)
if (res)
return res
}

return __require.call(this, id)
}

// patch Module.prototype.require to ensure mocked modules
// don't get loaded when Module._load the entry filePath module
Module.prototype.require = tapRequireMock

if (!Module._cache[filePath])
Module._load(filename, callerTestRef, false)
else
reload = true

const original = Module._cache[filePath]
const mod = Object.assign(
Object.create(Module.prototype),
this.original
original
)

const mock = this
mod.require = function tapRequireMock(id) {
const requiredFilePath = Module._resolveFilename(id, mock.original)
const res = mock.mocks.get(requiredFilePath)
if (res)
return res
// release global Module.prototype.require patch
Module.prototype.require = __require
delete Module._cache[filePath]
mod.require = tapRequireMock

return mock.original.require.call(this, id)
if (reload) {
mod.loaded = false
mod.load(filePath)
}

mod.loaded = false
mod.load(filePath)
this.module = mod.exports
}

Expand Down
45 changes: 25 additions & 20 deletions test/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,54 +30,59 @@ t.test('mock', t => {
},
lib: {
'a.js': `
const { inspect } = require('util');
const lorem = require('lorem');
const b = require('./b.js');
const c = require('./utils/c');
const d = require('../helpers/d.js');
const e = require('../e.cjs');
module.exports = function() {
return [inspect, lorem, b, c, d, e].map(i => i({})).join(' ')
};
`,
const { inspect } = require('util');
const lorem = require('lorem');
const b = require('./b.js');
const c = require('./utils/c');
const d = require('../helpers/d.js');
const f = require('../f.cjs');
module.exports = function() {
return [inspect, lorem, b, c, d, f, require('../g.js')]
.map(i => i({})).join(' ')
};
`,
'b.js': `module.exports = function () { return 'b' }`,
utils: {
'c.js': `module.exports = function () { return 'c' }`
},
},
helpers: {
'd.js': `module.exports = function () { return 'd' }`,
'd.js': `
const e = require('./e.js');
module.exports = function () { return ['d', e()].join(' ') }`,
'e.js': `module.exports = function () { return 'e' }`,
},
'e.cjs': `module.exports = function () { return 'e' }`,
'f.cjs': `module.exports = function () { return 'f' }`,
'g.js': `module.exports = function () { return 'g' }`,
})

t.equal(
Mock.get(resolve(__filename), resolve(path, 'lib/a.js'), {
[resolve(path, 'lib/b.js')]: () => 'foo',
})(),
'{} lorem foo c d e',
'{} lorem foo c d e f g',
'should use injected version of a mock',
)

t.equal(
require(resolve(path, 'lib/a.js'))(),
'{} lorem b c d e',
'{} lorem b c d e f g',
'should be able to use original module post-mocking',
)

t.equal(
Mock.get(resolve(__filename), resolve(path, 'lib/a.js'), {
[resolve(path, 'helpers/d.js')]: () => 'bar',
})(),
'{} lorem b c bar e',
'{} lorem b c bar f g',
'should mock module not located under the same parent folder',
)

t.equal(
Mock.get(resolve(__filename), resolve(path, 'lib/a.js'), {
[resolve(path, 'e.cjs')]: () => 'bar',
[resolve(path, 'f.cjs')]: () => 'bar',
})(),
'{} lorem b c d bar',
'{} lorem b c d e bar g',
'should mock module using cjs extension',
)

Expand All @@ -86,21 +91,21 @@ module.exports = function() {
[resolve(path, 'lib/b.js')]: () => 'foo',
[resolve(path, 'lib/utils/c')]: () => 'bar',
})(),
'{} lorem foo bar d e',
'{} lorem foo bar d e f g',
'should mock nested module',
)

t.equal(
Mock.get(resolve(__filename), resolve(path, 'lib/a.js'), {
util: { inspect: obj => obj.constructor.prototype },
})(),
'[object Object] lorem b c d e',
'[object Object] lorem b c d e f g',
'should mock builtin module',
)

t.equal(
require(resolve(path, 'lib/a.js'))(),
'{} lorem b c d e',
'{} lorem b c d e f g',
'should preserve original module after mocking',
)

Expand Down
33 changes: 33 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,28 @@ t.test('require defining mocks', t => {
)
})

t.test('immediately called require', t => {
const f = t.testdir({
'a.js': 'module.exports = () => { global.foo = true }',
'index.js': 'require("./a.js")()',
'test.js':
`const t = require('../..'); // tap
t.test('mock immediately called require', t => {
t.mock('./index.js', {
'./a.js': () => {
t.notOk(global.foo, 'should not run original a.js')
t.end()
}
})
})`,
})
return t.spawn(
process.execPath,
[ path.resolve(f, 'test.js') ],
{ cwd: f },
)
})

t.test('nested lib files', t => {
const f = t.testdir({
lib: {
Expand Down Expand Up @@ -1312,6 +1334,17 @@ t.test('require defining mocks', t => {
})
t.equal(c, 'mocked-a b d', 'should get expected mocked result')
t.end()
})
t.test('mock a mock', t => {
const i = t.mock('../index.js', {
'../lib/a.js': 'mocked-a',
'../lib/c.js': t.mock('../lib/c.js', {
'../lib/b': 'mocked-b-within-c'
})
})
t.equal(i(), 'mocked-a b a mocked-b-within-c d d', 'should get expected mocked-mocked result')
t.end()
})`,
},
})
Expand Down

0 comments on commit 51f45f5

Please sign in to comment.