question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

thenResolve() with a mocked object not working

See original GitHub issue

This was a “fun” one (if three days of pulling my hair out is fun, but I can’t get a haircut so I guess that’s OK 😃. I have an interface that has a method that returns a promise of another interface which is also mocked. When using thenResolve() (or thenReturn() with Promise.resolve()) any await on the promise fails to resolve. It works if any non-mocked object or primitive type is used.

So given (greatly simplfied):

interface Main {
  getChild(): Promise<Child>;
}

interface Child {
  name: string;
}

and then using this test (Mocha and Chai):

describe("mocking", function() {
  it("Should work", async function() {
        const mockedChild = mock<Child>();
        when(mockedChild.name).thenReturn("Sylvia");
        const child = instance(mockedChild);

        const mockedMain = mock<Main>();
        when(mockedMain.getPromise()).thenResolve(child);
        const main = instance(mockedMain);

        const result = await main.getPromise();  // <-- Here be dragons, this times outs
        console.log(`@@@@ val = '${result}'`);
        expect(result.name).to.equal("Sylvia");
  });
});

The test times out at the indicated line. I set the timeout to 10 seconds and it still times out.

Normally at this point I’d paste in more code, like the package.json etc. Or I’d create a separate, simplified project (which I did) and link it here (I can if you really want it) but I was so aggravated that I obviously didn’t understand what I was doing that I decided to figure out what I was doing wrong. It turns out, I wasn’t doing anything wrong, it was a slight by-product of the design of ts-mockito. I won’t call it a bug as everything is working exactly as designed. It’s more of an oversight of the subtleties of JavaScript and Promises.

BTW, one of the reasons I decided to figure it out on my own was because of another “bug” I’m going to report after this one pertaining to toString(). Another “fun” issue that made this one much harder for me.

So, what went “wrong.” If you read the Promise.resolve() description (I’m using the Mozilla one here because it’s easier to understand but I did read the JavaScript Spec and it says the same thing just using waaaaay more words) it says in part:

The Promise.resolve() method returns a Promise object that is resolved with a given value. If the value is a promise, that promise is returned; if the value is a thenable (i.e. has a “then” method), the returned promise will “follow” that thenable, adopting its eventual state; otherwise the returned promise will be fulfilled with the value. This function flattens nested layers of promise-like objects (e.g. a promise that resolves to a promise that resolves to something) into a single layer.

Promise.resolve() documentation

The bold is the interesting part. It says that it takes the resolve value, sees if it has a .then() method and if it does then uses the .then() method to resolve the value of the promise. Of course, because this is a mock of everything all of the objects that ts-mockito creates has a .then() method. Ruh-roh!

OK, now that is the problem. For bonus points, I have a solution. I think it’s right, and it does pass all the tests. It is in the attached PR. I have hand-patched my copy of ts-mockito and it works for me.

The basic solution is to have a list of what I’m calling defaultedPropertyNames which are properties that, if they are not explicitly set to return a value, will return undefined in the Proxy.get method. This is similar to the excludedPropertyNames in the .get() method that contains the hasOwnProperty value so we don’t override the one value we must have from JavaScript. The only difference is that my check comes after to check for a defined value for the property so if a when() is defined, that will be used.

BTW, in the PR there is another name in the defaultedPropertyNames list, Symbol(Symbol.toPrimitive) and that is to solve the next bug I’m opening, the fix was that same so I made them at the same time.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:17
  • Comments:23 (4 by maintainers)

github_iconTop GitHub Comments

35reactions
jamesharvcommented, Oct 15, 2020

Looking forward to this fix getting merged.

In the interim I am working around this problem by using a custom wrapper for the instance() function (resolvableInstance()), which wraps the ts-mockito Proxy in a new Proxy that returns undefined for the Promise interface methods that are causing the problem.

See my implementation below:

import { instance } from "ts-mockito";

export const resolvableInstance = <T extends {}>(mock: T) => new Proxy<T>(instance(mock), {
  get(target, name: PropertyKey) {
    if (["Symbol(Symbol.toPrimitive)", "then", "catch"].includes(name.toString())) {
      return undefined;
    }

    return (target as any)[name];
  },
});

Usage example:

import { resolvableInstance } from "./resolvableInstance";
import { expect } from "chai";
import { mock } from "ts-mockito";

class Service {
  public execute(): boolean {
    return false;
  }
}

class ServiceFactory {
  public async getService(): Promise<Service> {
    return new Service();
  }
}

describe("My test", () => {
  const serviceFactory = mock<ServiceFactory>();
  const service = mock<Service>();

  it("Can resolve a promise with a mock", () => {
    const serviceFactoryInstance = instance(serviceFactory);
    const serviceInstance = resolvableInstance(service);

    when(serviceFactory.getService()).thenResolve(serviceInstance);
    when(service.execute()).thenReturn(true)

    // This line hangs if serviceInstance was created with instance() rather than resolvableInstance().    
    const resolvedService = serviceFactoryInstance.getService();
    expect(resolvedService.execute()).to.eq(true);
  });
});
7reactions
NagRockcommented, Jul 3, 2020

Hey @jlkeesey Thanks for finding this and for the PR! Looks nice. I will play with it a bit and merge if not find issues.

Read more comments on GitHub >

github_iconTop Results From Across the Web

ts-mockito mocked promise never resolves - Stack Overflow
when location.get() is called, then never resolves, even on a 60s timeout, reject seems to work. What am I doing wrong?
Read more >
Mockito - Using Spies - Baeldung
In our example, the list object is not a mock. The Mockito when() method expects a mock or spy object as the argument....
Read more >
Testing your TypeScript code with ts-mockito | by Harijs Deksnis
This lets you specify what the mock should return when a particular method gets called. Besides thenReturn you can also specify thenThrow ,...
Read more >
@johanblumenberg/ts-mockito - npm
thenResolve() work for PromiseLike<T>; Make spying on an object prototype work ... Sometimes you need to mock a function, not an object, ...
Read more >
How to use the ts-mockito.mock function in ts-mockito - Snyk
Secure your code as it's written. Use Snyk Code to scan source code in minutes - no build needed - and fix issues...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found