Skip to content

Using testdouble esm loader with ts-node/esm loader on mocha leads to errors (Node 18.6.0) #496

Closed

Description

@AlexZ-343

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"
      }
    }

Activity

  1. searls commented on Jul 22, 2022

    @searls
    Member

    Could you please also make sure quibble is up-to-date and share its version? Tagging @giltayar if he knows about any of this

  2. giltayar commented on Jul 25, 2022

    @giltayar
    Contributor

    @AlexZ-343 would love to get a reproduction repository so that I can debug it this week! It would make my life much easier.

  3. AlexZ-343 commented on Jul 25, 2022

    @AlexZ-343
    Author

    quibble is 0.6.13, which is the latest release. Working on the reproduction repository and I will link it when ready.

  4. AlexZ-343 commented on Jul 25, 2022

    @AlexZ-343
    Author

    I have the repo set is private. I have added @searls and @giltayar to it.

    https://github.com/AlexZ-343/testdouble-issue

  5. giltayar commented on Jul 26, 2022

    @giltayar
    Contributor

    Reproduced! Working on it...

  6. giltayar commented on Jul 26, 2022

    @giltayar
    Contributor

    Found the bug! Fixing...

  7. AlexZ-343 commented on Aug 2, 2022

    @AlexZ-343
    Author

    Hi @giltayar, what's the status on the resolution? I see a fix has been fixed, but there hasn't been a release yet?

  8. AlexZ-343 commented on Aug 4, 2022

    @AlexZ-343
    Author

    Checking in again @giltayar @searls

  9. alichry commented on Aug 6, 2022

    @alichry

    Hi @AlexZ-343, I'm facing this issue as well. There's a fix by the node team in nodejs/node#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.

  10. alichry commented on Aug 7, 2022

    @alichry

    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

  11. giltayar commented on Aug 8, 2022

    @giltayar
    Contributor

    @searls did you create a release?

  12. searls commented on Aug 8, 2022

    @searls
    Member

    @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?

  13. AlexZ-343 commented on Aug 9, 2022

    @AlexZ-343
    Author

    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

    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.

  14. AlexZ-343 commented on Aug 9, 2022

    @AlexZ-343
    Author

    @giltayar @searls let me know when you guys are able to make a release.

  15. alichry commented on Aug 10, 2022

    @alichry

    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.

  16. searls commented on Aug 10, 2022

    @searls
    Member

    Try updating to quibble@0.6.14

  17. alichry commented on Aug 10, 2022

    @alichry

    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?

  18. searls commented on Aug 10, 2022

    @searls
    Member

    Not sure, just wanted to make sure you were up to day. @giltayar?

  19. AlexZ-343 commented on Aug 10, 2022

    @AlexZ-343
    Author

    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

  20. giltayar commented on Aug 10, 2022

    @giltayar
    Contributor

    @AlexZ-343 looking into it.

  21. giltayar commented on Aug 10, 2022

    @giltayar
    Contributor

    @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 :-)

  22. AlexZ-343 commented on Aug 10, 2022

    @AlexZ-343
    Author

    @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.

  23. giltayar commented on Aug 11, 2022

    @giltayar
    Contributor

    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.

  24. AlexZ-343 commented on Aug 23, 2022

    @AlexZ-343
    Author

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions