Skip to content

Commit

Permalink
Add t.mockAll method
Browse files Browse the repository at this point in the history
Fix: #790
  • Loading branch information
isaacs committed Sep 14, 2023
1 parent d7e7e4f commit 2c135b0
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 6 deletions.
26 changes: 22 additions & 4 deletions src/mock/README.md
Expand Up @@ -21,13 +21,15 @@ import t from 'tap'
t.test('handls stat failure by throwing', async t => {
const mockStatSync = (p: string) => {
t.equal(p, 'filename.txt')
throw Object.assign(new Error('expected error'), { code: 'ENOENT' })
throw Object.assign(new Error('expected error'), {
code: 'ENOENT',
})
}
// do 'as typeof import(...)' so that TS knows what it returns
const thingThatDoesStat = await t.import(
const thingThatDoesStat = (await t.import(
'../dist/my-statty-thing.js',
{ 'node:fs': { statSync: mockStatSync } }
) as typeof import('../dist/my-statty-thing.js')
)) as typeof import('../dist/my-statty-thing.js')

t.throws(() => thingThatDoesStat('filename.txt'), {
message: 'expected error',
Expand All @@ -38,7 +40,7 @@ t.test('handls stat failure by throwing', async t => {

### `t.mockImport(module, [mocks]): Promise<any>`

Load the module with `import()`. If any mocks are provided, then
Load the module with `import()`. If any mocks are provided, then
they'll override the module's imported deps. This works for both
ESM and CommonJS modules.

Expand All @@ -48,6 +50,22 @@ Same as `t.mockImport()`, but synchronously using `require()`
instead. This only works with CommonJS, and only mocks CommonJS
modules loaded.

### `t.mockAll(mocks: Record<string,any> | null): Record<string, any>`

Convenience method to set the mocks for all subsequent calls to
`t.mockRequire` or `t.mockImport` for the remainder of the test.

Mocks added with `mockAll` are overridden by any explicit mocks
set in the `t.mockRequire` or `t.mockImport` call.

Repeated calls to `t.mockAll()` will _add_ mocks to the set. If the same
name is used again, it will replace the previous value, not merge.

If a key is set to `undefined` or `null`, then it will be removed from
the `mockAll` set.

Reset by calling `t.mockAll(null)`

### `t.createMock(originalModule, mockOverrides): mockedModule`

Sometimes you only want to override one function or property,
Expand Down
35 changes: 33 additions & 2 deletions src/mock/src/index.ts
Expand Up @@ -15,6 +15,8 @@ export class TapMock {
#didTeardown: boolean = false
#mocks: MockService[] = []

#allMock: Record<string, any> = {}

constructor(t: TestBase) {
this.#t = t
}
Expand Down Expand Up @@ -125,7 +127,8 @@ export class TapMock {
*
* @group Spies, Mocks, and Fixtures
*/
mockImport(module: string, mocks: { [k: string]: any } = {}) {
mockImport(module: string, mocks: Record<string, any> = {}) {
mocks = Object.assign({}, this.#allMock, mocks)
if (!this.#didTeardown && this.#t.t.pluginLoaded(AfterPlugin)) {
this.#didTeardown = true
this.#t.t.teardown(() => this.unmock())
Expand Down Expand Up @@ -158,10 +161,38 @@ export class TapMock {
*
* @group Spies, Mocks, and Fixtures
*/
mockRequire(module: string, mocks: { [k: string]: any } = {}) {
mockRequire(module: string, mocks: Record<string, any> = {}) {
mocks = Object.assign({}, this.#allMock, mocks)
return mockRequire(module, mocks, this.#t.t.mockRequire)
}

/**
* Set some mocks that will be used for all subsequent
* {@link @tapjs/mock!index.TapMock#mockImport} and
* {@link @tapjs/mock!index.TapMock#mockRequire} calls made by this test.
*
* Mocks added with `mockAll` are overridden by any explicit mocks set in the
* `t.mockRequire` or `t.mockImport` call.
*
* Repeated calls to `t.mockAll()` will *add* mocks to the set. If the same
* name is used again, it will replace the previous value, not merge.
*
* If a key is set to `undefined` or `null`, then it will be removed from
* the `mockAll` set.
*
* Reset by calling `t.mockAll(null)`
*/
mockAll(mocks: Record<string, any> | null): Record<string, any> {
if (mocks === null) this.#allMock = {}
else {
this.#allMock = Object.assign(this.#allMock, mocks)
for (const [k, v] of Object.entries(this.#allMock)) {
if (v === undefined || v === null) delete this.#allMock[k]
}
}
return this.#allMock
}

/**
* Unwind the mocks and free up the memory at the end of the test.
*
Expand Down
48 changes: 48 additions & 0 deletions src/mock/test/index.ts
Expand Up @@ -27,6 +27,24 @@ t.test('mockRequire', t => {
t.end()
})

t.test('mockRequire with mockAll', t => {
const dir = t.testdir({
'file.cjs': `
exports.foo = require('./foo.cjs')
`,
'foo.cjs': `
module.exports = 'original foo'
`,
})
const rel = './' + relative(__dirname, dir) + '/'
t.mockAll({
[rel + 'foo.cjs']: 'mocked foo',
})
const mocked = t.mockRequire(rel + 'file.cjs')
t.strictSame(mocked, { foo: 'mocked foo' })
t.end()
})

t.test('deprecated t.mock alias', t => {
const logs = t.capture(console, 'error')
const dir = t.testdir({
Expand Down Expand Up @@ -70,6 +88,25 @@ t.test('mockImport', async t => {
t.end()
})

t.test('mockImport with mockAll', async t => {
const dir = t.testdir({
'file.mjs': `
import f from './foo.mjs'
export const foo = f
`,
'foo.mjs': `
export default 'original foo'
`,
})
const rel = './' + relative(__dirname, dir) + '/'
t.mockAll({
[rel + 'foo.mjs']: 'mocked foo',
})
const mocked = await t.mockImport(rel + 'file.mjs')
t.strictSame(mocked, { __proto__: null, foo: 'mocked foo' })
t.end()
})

t.test('createMock', t => {
const dir = t.testdir({
'file.cjs': `
Expand Down Expand Up @@ -120,3 +157,14 @@ t.test('createMock array', t => {
t.strictSame(t.createMock([1, 2, 3], [3, 2, 1]), [3, 2, 1])
t.end()
})

t.test('mockAll editing', t => {
t.strictSame(t.mockAll({ a: 'blah' }), { a: 'blah' })
t.strictSame(t.mockAll({ b: 'blah' }), { a: 'blah', b: 'blah' })
const x = t.mockAll({ c: 'c', b: undefined })
t.strictSame(new Set(Object.keys(x)), new Set(['a', 'c']))
const y = t.mockAll({ c: null })
t.strictSame(Object.keys(y), ['a'])
t.strictSame(t.mockAll(null), {})
t.end()
})

0 comments on commit 2c135b0

Please sign in to comment.