0

Given a module under test sut.js

const { dependencyFunc } = require('./dep')

module.exports = () => {
  return dependencyFunc()
}

with dependency dep.js

module.exports = {
  dependencyFunc: () => 'we have hit the dependency'
}

and some tests:

describe('mocking in beforeEach', () => {
  let sut
  let describeScope

  beforeEach(() => {
    let beforeEachScope = false
    describeScope = false

    console.log('running before each', { beforeEachScope, describeScope })
    jest.setMock('./dep', {
      dependencyFunc: jest.fn().mockImplementation(() => {
        const returnable = { beforeEachScope, describeScope }
        beforeEachScope = true
        describeScope = true
        return returnable
      })
    })
    sut = require('./sut')
  })

  it('first test', () => {
    console.log(sut())
  })

  it('second test', () => {
    console.log(sut())
  })
})

I get the following output:

me$ yarn test test.js
yarn run v1.22.5
$ jest test.js
 PASS  ./test.js
  mocking in beforeEach
    ✓ first test (17 ms)
    ✓ second test (2 ms)

  console.log
    running before each { beforeEachScope: false, describeScope: false }

      at Object.<anonymous> (test.js:9:13)

  console.log
    { beforeEachScope: false, describeScope: false }

      at Object.<anonymous> (test.js:22:13)

  console.log
    running before each { beforeEachScope: false, describeScope: false }

      at Object.<anonymous> (test.js:9:13)

  console.log
    { beforeEachScope: true, describeScope: false }

      at Object.<anonymous> (test.js:26:13)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.248 s, estimated 2 s
Ran all test suites matching /test.js/i.
✨  Done in 3.56s.

I expect the output for both tests to be { beforeEachScope: false, describeScope: false }. I.e., I expect both beforeEachScope and describeScope variables to be reset to false regardless of whether they were declared in the beforeEach scope or the describe scope. In my real test I consider it cleaner to have it in the beforeEach scope as it is not needed elsewhere. What's going on? What scope is the Jest stuff using?

sennett
  • 8,014
  • 9
  • 46
  • 69

1 Answers1

1

I looks like setMock only takes the first mock for a given module into account, and does not overwrite it on the second call. Or rather, I believe, it is the require doing the caching - jest empties the module cache only once before running the entire test suite (it is expected that the imports will be at the top of the suite, after declaring which modules to mock).

Your mock implementation then has a closure over the beforeEachScope from the first beforeEach call.

Why, you may wonder, does it not appear to have a closure over the describeScope as well? In fact, it does, what might be confusing in your code is that the beforeEach does run describeScope = false which always resets it to false before it gets logged anywhere. If you remove that statement, and instead only initialise let describeScope = false in the describe scope, you'll see that it will change to true after the first sut() invocation as well.

Here's what's happening if we resolve the scopes manually and remove all the jest wrappers from the execution:

let sut
let describeScope

// first test, beforeEach:
let beforeEachScope1 = false
describeScope = false

console.log('running before each 1', { beforeEachScope1, describeScope }) // false, false as expected
jest.setMock('./dep', {
  dependencyFunc(n) {
    console.log('sut call '+n, { beforeEachScope1, describeScope });
    beforeEachScope1 = true
    describeScope = true
  })
})
sut = require('./sut') // will call the function we just created

// first test
sut(1) // still logs false, false

// second test, beforeEach:
let beforeEachScope2 = false // a new variable
describeScope = false // reset from true to false, you shouldn't do this

console.log('running before each 2', { beforeEachScope2, describeScope }) // logs false, false
jest.setMock('./dep', {
  dependencyFunc(n) {
    // this function is never called
  })
})
sut = require('./sut') // a no-op, sut doesn't change (still calling the first mock)

// second test:
sut(2) // logs true (beforeEachScope1) and false

Use the following:

const dependencyFunc = jest.fn();
jest.setMock('./dep', {
  dependencyFunc,
})
const sut = require('./sut')

describe('mocking in beforeEach', () => {
  let describeScope = false

  beforeEach(() => {
    let beforeEachScope = false

    console.log('running before each', { beforeEachScope, describeScope })
    dependencyFunc.mockImplementation(() => {
      const returnable = { beforeEachScope, describeScope }
      beforeEachScope = true
      describeScope = true
      return returnable
    })
  })

  it('first test', () => {
    console.log(sut())
  })

  it('second test', () => {
    console.log(sut())
  })
})

The following demonstrates the combined caching and scope / closure behaviour.

let cachedFunction

let varInGlobalClosure

const run = () => {
  varInGlobalClosure = false
  let varInRunClosure = false

  // next line is what jest.mock is doing - caching the function
  cachedFunction = cachedFunction || (() => {
    const returnable = { varInRunClosure, varInGlobalClosure }
    varInRunClosure = true
    varInGlobalClosure = true
    return returnable
  })

  return cachedFunction
}

console.log('first run', run()()) // outputs { varInRunClosure: false, varInGlobalClosure: false }
console.log('second run', run()()) // outputs { varInRunClosure: true, varInGlobalClosure: false }

This is because we are making a new closure insiderun, with a new varInRunClosure when we call run for the second time, but the cached function is still using the closure generated by the first time run ran, which is now inaccessible outside of the cached function scope.

sennett
  • 8,014
  • 9
  • 46
  • 69
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Hi Bergi thanks for your reply. In my real test I'm mocking a database repository, and I want to reset the data between tests - `describeScope = false` and `beforeEachScope = false` are me resetting the data in this illustration of the issue I'm facing. I don't need to access the internals of the mock data repository during the test. Why doesn't the data (represented here using `beforeEachScope` and `describeScope`) reset if the variable is declared in the `beforeEach` closure? – sennett Oct 30 '20 at 08:52
  • It does. What doesn't reset to a new mocked module is the `sut = require('./sut')`. You get the same module as from the first `require` call, with the old data. – Bergi Oct 30 '20 at 11:24
  • I'm sorry I'm not following. Is the data is stored in the scope of the closure (either beforeEach or describe) regardless of whether require caches the module, as that's where the variables were initialised? If this is not the case, why does the closure with beforeEach work differently from the closure with describe? – sennett Oct 30 '20 at 19:00
  • Yes, the variables are stored in the scopes in which you declared them, there is no magic going on. They work differently because you reset `describeScope = false` but not `beforeEachScope` in your `beforeEach`. – Bergi Oct 30 '20 at 21:56
  • Thanks so much for the code examples and continued help - it's clear now. Closures work slightly differently from how I expected. I've requested an edit to your answer which distills things down to just the closure. – sennett Nov 01 '20 at 10:19