Stubbing exported function with sinon, mocha, and swc fails

632 views Asked by At

I am trying to stub the function exported from my ES module.

I used wildcard import(import * as) to stub it, and it works when ts-node is used for transpiling(mocha --require ts-node/register */**/*.spec.ts).

But when swc is used, it fails with the message below(mocha --require @swc/register */**/*.spec.ts).

TypeError: Descriptor for property validate is non-configurable and non-writable
/* hash.ts */
import * as argon2 from 'argon2'

export async function encrypt(plain) {
  return await argon2.hash(plain)
}

export async function validate(hash, plain) {
  return await argon2.verify(hash, plain)
}
/* service.ts */
import { validate } from './hash'

export async function isValidUser(user: User, password: string) {
  if (!user || !(await validate(user.password, password))) {
    return false
  }
  return true
}
/* service.spec.ts */
import * as hash from './hash'
import { isValidUser } from './service'
import { stub } from 'sinon'

describe('isValidUser', () => {
  stub(hash, 'validate').callsFake(
    async (passwordFormDB, passwordFromUserInput) =>
      passwordFromDB === passwordFromUserInput
  )

  it('...', async () => {
    /* test `isValidUser` function */
  })
})
// .swcrc
{
  "test": ".*.ts$",
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "decorators": true,
      "importMeta": true
    },
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true
    },
    "paths": {
      "src/*": ["./src/*"]
    }
  },
  "module": {
    "type": "commonjs",
    "noInterop": true
  }
}
1

There are 1 answers

0
oligofren On

Since a similar issue just popped up on the Sinon bug tracker I decided to dive down and see if I can solve it. Turns out it's not that hard, but if you want some details on the background you should check out the issue. I'll just copy-paste the relevant bits here.

Sinon makes mocks, stubs and spies. Those are plain, normal functions, and we do not do any magic to the runtime, so whatever is to be done needs to be done using normal Javascript in the given runtime. Sinon clearly states what the issue is:

Descriptor for property validate is non-configurable and non-writable

If the transpiled code restricts everyone from modifying those exports, Sinon cannot do anything by itself. This issue is not an issue with Sinon, but with your transpiler tooling.

While ts-node clearly exports a writeable object descriptor, SWC does not. This is in-line with how ES Modules are supposed to work, so SWC seems to display the correct behavior here.

Solution

Since the runtime is not using ESM, but CommonJS after transpilation with SWC, you can employ any of the normal link seam approaches for CommonJS where you basically intercept module loading. The Sinon homepage lists one such approach, using Proxiquire. You could also use Rewire, Quibble (of TestDouble) or other tools that basically do the same.

describe('isValidUser', () => {
    const mySpy = sinon.spy(async (passwordFormDB, passwordFromUserInput) =>
        passwordFromDB === passwordFromUserInput)
    quibble('./hash', { validate: mySpy });

    it('...', async () => {
        /* test `isValidUser` function */
    })
})

The linked issue also shows how to replace modules if you are not using TypeScript, but running untranspiled ESM, by using Quibble as the module loader. It also explains why this is currently hard (not impossible) to mock modules when targeting ES Modules ("moduleResolution": "Node16") using TypeScript, as you need to write a delegating loader to handle resolution of filenames before handing over to Quibble.