Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed problem with a mock resolving a mocked value with Promises #194

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

jlkeesey
Copy link
Contributor

The long description is in #191 so this is short.

This is a replacement for #192 which was incorrect.

This fixes a problem where returning a mocked object through thenResolve() on another mocked object never resolves.

By default, all symbols except .hasOwnProperty() are stubbed even if
they are not assigned a value. This is to be able to track references to
all invocations. However, the way that Promise.resolve() works, it
checks for the presence of a .then() property and if it is there assumes
that the resolve value is a promise so it tries to get its resolved
value. But this is a mock reference so the stub does not return a
resolved value so the promise never resolves.

This fix excludes .then() and .catch() from automatically creating a
stub so that Promise will not try to resolve it, but the user can still
override the call with a when() clause.

This fix also adds "Symbol(Symbol.toPrimitive)" to the exclude list so
that by default toString() on a mock will return something other than
null.
Added test to verify that adding "Symbol(Symbol.toPrimitive)" to the
exclude list will allow toString() conversion to work properly.
In the prior commit, the location of one of the new tests was in the
wrong location. This prevented the code from working and the coverage
went down. Moved the test to the correct location and everything passes
and the coverage went up-- at least on my computer.
@ianw11
Copy link

ianw11 commented Aug 23, 2020

Also commented on #191 hoping to bring visibility to this fix!

@MOHACGCG
Copy link

MOHACGCG commented Oct 1, 2020

@jlkeesey seems like this is approved thanks to @Elecweb. Can't wait for it to be merged! Thank you for the continued contribution on this great project!

@Napas
Copy link

Napas commented Oct 7, 2020

Hi, when this is going to be merged in?

@alex-e-c
Copy link

Any update on this?

@alex-faber
Copy link

Hi, when will this be merged? Loads of ignored tests at the moment :( haha

@MOHACGCG
Copy link

MOHACGCG commented Oct 15, 2020

@Napas @alex-e-c @alex-faber while waiting for the merge, as a workaround you can wrap the mocked object.
instead of when(foo()).thenResolve(instance(boo))
use:
alternative 1:
when(foo()).thenResolve({boo: instance(boo)})
alternative 2
when(foo()).thenResolve({ ... } as boo)

not the best but at least you can run the tests.

@alex-faber
Copy link

@MOHACGCG I'm pretty sure when(foo()).thenResolve(instance(boo)) is what I do anyway, and it never resolves

@MOHACGCG
Copy link

MOHACGCG commented Oct 20, 2020

@MOHACGCG I'm pretty sure when(foo()).thenResolve(instance(boo)) is what I do anyway, and it never resolves

correct, what i meant is the alternatives do resolve, you can use them as a workaround for now.

@alex-faber
Copy link

@MOHACGCG sorry my bad, I misread! I'll give it a go, thanks!

@MOHACGCG
Copy link

@MOHACGCG sorry my bad, I misread! I'll give it a go, thanks!

no worries. good luck. I edited the comment to make it more clear!

@algono
Copy link

algono commented Oct 27, 2020

@Napas @alex-e-c @alex-faber @MOHACGCG The workaround from this comment #191 (comment) worked for me, with no need to alter the resolved value's structure. I hope it works for you as well.

@BenjaminPalko
Copy link

Any update on this?

@RageCage64
Copy link

Any reason this hasn't been merged in? The workaround is fine, but this would be a good fix to have in the library.

@upMKuhn
Copy link

upMKuhn commented Sep 12, 2021

Same question :D "Any reason this hasn't been merged in? The workaround is fine, but this would be a good fix to have in the library."

@donnyroufs
Copy link

ola what's up with this? time to merge this bad boy

@donnyroufs
Copy link

Same question :D "Any reason this hasn't been merged in? The workaround is fine, but this would be a good fix to have in the library."

Looks like we gotta fork and just publish it ourselves on npm ;')

@upMKuhn
Copy link

upMKuhn commented Sep 19, 2021

Hi @NagRock @mikeporterdev,

Thank you for this library! This issue is a real obstacle for new users and a nuisance for anyone using this library with nestjs. Could you please look into this github issue.

A summary: NestJs thinks mocks are a promise and waits indefinitely. To fix this when((mock as any).then).thenReturn(undefined) has to be set up everytime for nearly every mock.

Thank you!

@NagRock
Copy link
Owner

NagRock commented Nov 17, 2021

Hi, thanks for PR. But wouldn't this break anything if someone would like to mock then function?

@dariosn85
Copy link

@NagRock I think there are at least two sides of the problem:

1. Class has own method then

Let's have a look to following code as an example:

class Student {
    public test(): void {
        // do something
    }
}

In such case you concern make sense. Fix would break this logic in case if someone wants to mock then method of Student class.

2. Class does not have own then method, but then method is part of other function or comment

Now see following example:

class Person {
    number: number;
    
    public getName(): Promise<string> {
        return this.loadName()
            .then(name => {
                return `Student name: ${name}`;
            });
    }
    
    public async getPhoneNumber(): Promise<number> {
        
        // commented out code which also causes then() mock
        // await this.loadNumber()
        //     .then(number => this.number)
        //     .catch(error => this.number = undefined);
        
        return this.number;
    }
    
    private loadNumber(): Promise<number> {
        // do something
    }
    
    private loadName(): Promise<string> {
        // do something
    }
}

Because of nature how MockableFunctionsFinder works, it would find out .then() from .getName() and .then() from commented out code (within getPhoneNumber()) as a candidate for mocked method.


I would say that not all, but most cases what happens on the field is described by "2." (Person class). I think also we can all agree that in normal circumstances Person mock should not contain then method.

Is there any particular reason why ts-mockito class mocking is done by calling toString on the class and then searching for methods using regex? I would like to understand why base for mocking are not prototype methods or similar instead?

If the reason for regex is the way how method parameters are looked up or similar, then I would propose some kind of hybrid solution. We could combine class prototype/own methods (base for mocking) with output of regex (additional information). In addition ts-mockito could print out warning in case if class has own then method. That way we would cover both scenarios from above and at least give user clue (warning) what is happening.

@devtronic
Copy link

This workaround works for me:

// This import is not that nice but it's the only way to create mocks that works with promises
import {Mocker} from "ts-mockito/lib/Mock";

export function mockForPromise(clazz: unknown, instance?: unknown): any {
    return new Mocker(clazz, instance).getMock();
}

// use it
const mockApp = mockForPromise(Model<AppEntity>);
when(mockAppRepository.getAppByPackageId("test-package-id")).thenResolve(instance(mockApp));

@ffMathy
Copy link

ffMathy commented Apr 13, 2023

@NagRock it is so painful for this to wait several years when a fix is right there in front of our eyes. If you've abandoned the project, can you at least appoint a contributor to take over?

@ffMathy
Copy link

ffMathy commented Apr 13, 2023

Hi, thanks for PR. But wouldn't this break anything if someone would like to mock then function?

To respond to this: Yes. However, it's inevitable. There's no better way to do this when working with proxies.

I think the benefits outweigh the downsides. Especially because mocking "then" and "catch" is unlikely - way more unlikely than working with promises.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet