Using testdouble esm loader with ts-node/esm loader on mocha leads to errors (Node 18.6.0)
Description
I am trying to do esm mocking using testdouble with a mixed Typescript and Javascript project, mostly following this post
Node 18.6.0 now allows for chaining of loads, and I previously needed ts-node/esm in order to recognize my Typescript files and imports.
I realize that this new feature is less than a week old, but I would appreciate support with reconciling other loaders that I would need for my es6 project (or to find a way that only requires one loader). Due to project requirements, using commonjs on runtime is not an option.
Issue
When running the following mocha command:
NODE_OPTIONS='--experimental-specifier-resolution=node --loader=ts-node/esm --loader=testdouble' mocha test/.js test/.spec.ts
and any permutation thereof (removing experimental-specifier-resolution, changing the order of the --loader params)
I get the following exception
Exception in PromiseRejectCallback: node:internal/modules/esm/loader:178 return output; RangeError: Maximum call stack size exceeded Exception in PromiseRejectCallback: /Users/repos/moRepo/node_modules/ts-node/dist/esm.js:120 const { source: rawSource } = await defaultLoad(url, { ^ RangeError: Maximum call stack size exceeded at validateArgs (node:internal/modules/esm/loader:578:26)
These 4 lines repeat indefinitely:
at addShortCircuitFlag (/Users/repos/myRepo/node_modules/ts-node/src/esm.ts:409:21) at load (/Users/repos/myRepo/node_modules/ts-node/src/esm.ts:239:12) at nextLoad (node:internal/modules/esm/loader:173:28) at /Users/repos/myRepo/node_modules/ts-node/src/esm.ts:255:45
Environment
node v18.6.0 npm v.8.13.2 testdouble v.3.16.6
Example Repo
I don't have a repo handy, but I can try to create one and add it in the future.
Code-fenced Examples
service_one.ts
export function getAllInvalidObjects(connection: Connection) {
const response: string[] = service_two.getData(someString);
... // some transformations of response
return response;
}
service_two.ts
// this is the function I'd like to mock when testing service_one.ts
export function getData(someString) {
axios.get('https://endpoint.com/path?string=' + someString).then((response: any) {
resolve(response.json);
}
}
test.ts :
import * as td from 'testdouble';
let mock_validation_rest: any;
beforeEach(async () => {
mock_rest = await td.replaceEsm('../src/services/service_two.js');
});
afterEach(function () {
sandbox.restore();
td.reset();
});
it('successfully validates with mocking, async () => {
td.when(mock_rest.prototype.getData(td.matchers.anything())).thenResolve(mockData);
// as long as the mock above works then this test will pass
return service_one.getAllInvalidObjects(connection).then((response: string[]) => {
assert.equal(3, response.length);
});
});
package.json:
{
"type": "module",
"scripts": {
"test": NODE_OPTIONS='--experimental-specifier-resolution=node --loader=ts-node/esm --loader=testdouble' mocha test/*.js test/*.spec.ts -r dotenv/config
}
}
tsconfig.json:
{
"compilerOptions": {
"target": "es2016",
"module": "es6,
"moduleResolution": "node16"
"allowJs": true,
"esModuleInterop": true
},
"ts-node": {
"esm": true
}
"include": [
"./src/**/*",
"test/**/*/.ts",
"test/**/*.js"
}
}
Could you please also make sure quibble is up-to-date and share its version? Tagging @giltayar if he knows about any of this
@AlexZ-343 would love to get a reproduction repository so that I can debug it this week! It would make my life much easier.
quibble is 0.6.13, which is the latest release. Working on the reproduction repository and I will link it when ready.
I have the repo set is private. I have added @searls and @giltayar to it.
https://github.com/AlexZ-343/testdouble-issue
Reproduced! Working on it...
Found the bug! Fixing...
Hi @giltayar, what's the status on the resolution? I see a fix has been fixed, but there hasn't been a release yet?
Checking in again @giltayar @searls
Hi @AlexZ-343, I'm facing this issue as well. There's a fix by the node team in https://github.com/nodejs/node/pull/44109. I am going to test it using node v19.0.0-nightly20220806760ecc9c75/ that includes it. if I find anything, I'll let you know.
I've created a playground here: testdouble-ts-node-esm-playground. Mocking fs succeeded, however, mocking internal modules written in typescript did not. The outcome is identical when using node v18.7.0 or node v19.0.0-nightly20220806760ecc9c75
@searls did you create a release?
@giltayar no, I'd actually invited you to after adding you as a maintainer on the npm package, along with an updated RELEASE.md doc!
Would you like to try your hand at releasing a patch bump for this?
I've created a playground here: testdouble-ts-node-esm-playground. Mocking
fssucceeded, however, mocking internal modules written in typescript did not. The outcome is identical when using nodev18.7.0or nodev19.0.0-nightly20220806760ecc9c75
hey @alichry, thanks for taking a look at this!
Could you clarify what you mean by this statement?
mocking internal modules written in typescript did not
I'll try playing around with it a bit on my end as well.
@giltayar @searls let me know when you guys are able to make a release.
Could you clarify what you mean by this statement?
Hi @AlexZ-343 👋, whoops, sorry about that. internal is a bit ambiguous since internal modules often refer to internal nodejs modules. Nonetheless, I meant to imply that, when using the ts-node/esm loader and testdouble loader, mocking ES modules written in TypeScript did not work.
$ yarn test:ts
yarn run v1.22.18
warning You are using Node "19.0.0-nightly20220806760ecc9c75" which is not supported and may encounter bugs or unexpected behavior. Yarn supports the following semver range: "^4.8.0 || ^5.7.0 || ^6.2.2 || >=8.0.0"
$ NODE_OPTIONS='--loader=ts-node/esm --loader=testdouble' mocha src/**/*.spec.ts
(node:30884) ExperimentalWarning: Custom ESM Loaders is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:30884) DeprecationWarning: Obsolete loader hook(s) supplied and will be ignored: getFormat, getSource
Memory
1) Should mock ps
Register
✔ Should mock fs
1 passing (66ms)
1 failing
1) Memory
Should mock ps:
AssertionError [ERR_ASSERTION]: 10000 == 700
+ expected - actual
-10000
+700
at Context.<anonymous> (file:///private/tmp/quibble-ts-node-test/src/memory.spec.ts:21:16)
at Generator.next (<anonymous>)
at fulfilled (file:///private/tmp/quibble-ts-node-test/src/memory.spec.ts:4:58)
error Command failed with exit code 1.
Based on the above, mocking fs succeeded but not ./ps.ts
https://github.com/alichry/testdouble-ts-node-esm-playground/blob/d3de64aafd29294b54f9140548a3904509b2b326/src/register.spec.ts#L7-L12 https://github.com/alichry/testdouble-ts-node-esm-playground/blob/d3de64aafd29294b54f9140548a3904509b2b326/src/memory.spec.ts#L7-L21
yarn test:js succeeds. test:js transpiles the source code and test files written in ts into ESM, and runs the *.spec.js test files. This is a convenient workaround.
Try updating to [email protected]
Hey @searls, thank you for the release! I've set it to 0.6.14 instead of the fix's commit hash. I've noticed no changes whatsoever; what am I missing?
Not sure, just wanted to make sure you were up to day. @giltayar?
Hey @searls and @giltayar , I tried the latest version of quibble with my dummy repo and while I am no longer seeing the lengthy exception message, I am having some trouble getting a proper mock.
When I try mocking with prototype, I get this:
td.when(mock_json_placeholder_rest.prototype.getPostById(td.matchers.anything())).thenResolve(response);
TypeError: Cannot read properties of undefined (reading 'getPostById')
If I try to do it without prototype:
td.when(mock_json_placeholder_rest.getPostById(td.matchers.anything())).thenResolve(response);
It acts like there is no mock (which I confirmed with some console logging).
Is there an error in how I'm setting it up?
Updated dummy repo for reference: https://github.com/AlexZ-343/testdouble-issue
@AlexZ-343 looking into it.
@AlexZ-343 this was an interesting one. There are two problems in your test code. One which is obvious, and one which is definitely not. Let's start with the obvious:
You're calling td.replaceEsm('../services/json-placeholder-rest.js') after the import * as getPostTitles from '../get_post_service.js'. So get_post_service.js module has already imported the non-mocked version of json-placeholder-rest.js.
To fix this, remove import * as getPostTitles from '../get_post_service.js' from the top and replace it with a getPostTitles = await import('../get_post_service.js'); after the td.replaceEsm(...). This will ensure that when get_post_service.js imports json-placeholder-rest.js it will import the mocked version.
OK, that's the regular one. Now for the VERY non-obvious one, which got me stumped. Let's start by how to fix. Replace td.replaceEsm('../services/json-placeholder-rest.js') with... td.replaceEsm('../services/json-placeholder-rest.ts') (i.e. replace .js by .ts).
Why? Because the ts-node/esm loader replaces the module resolution of the json-placeholder-rest.js file with the path that ends with .ts. And because the testdouble loader comes after ts-node/esm, then the import it receives is to the .ts file, which it hasn't mocked! (because the mock was for the .js file)
Totally non-obvious, but this is a really interesting interplay with multiple loaders here. We're probably going to see a lot fo those in the future, and now I will probably be able to figure them out without console.log-ing stuff in quibble :-)
@giltayar thanks for walking me through this.
I tried the fixes, and I still get:
TypeError: Cannot read properties of undefined (reading 'getPostById')
This is with .prototype (prototype is correct, right? Without it, there's no mock at all).
Is there something else I'm missing, like the exact ordering of the import or something?
I updated the repo with my attempted changes.
Oh, I fogort: the prototype thing I didn't understand and removed. Why would there be a prototype? It's just a function exported from a module.
Hey all, sorry for the delay. I haven't had a chance to test the fix extensively.
I did try it for a unit test or two and it looks good! Personally, I'd like to do a few more test cases before closing this, but I think this should be fixed at this point.