EmandM / ts-mock-imports

Intuitive mocking library for Typescript class imports
MIT License
109 stars 8 forks source link

Mocking class properties #33

Closed Vogel612 closed 3 years ago

Vogel612 commented 3 years ago

I'm currently trying to write unit-tests involving a library exporting a Class instance that exposes all the functionality I want to access behind properties. Basically my system under test looks somewhat like this:

class SUT {
    let client;
    constructor(config) {
        this.client = new Client(config.get(...));
    }

    async doSomething() {
        const result = await this.client.someProp.someFn(...);
        // filter result and return afterwards
    }
}

Unfortunately I have been unable to mock the dependency out using ImportMock.mockClass. The problem seems to be that the code does not invoke the mocked property as a function. I'd initially tried to replace client.someProp.someFn() by using

client = ImportMock.mockClass(clientMod, "Client");
const assertStub = stub();
client.mock("someProp", { someFn: assertStub }); 

This fails with the error message "someFn is not a function". Debugging into the SUT reveals that someProp is replaced with a function instead of a property. To remedy this I attempted to change the mock of "someProp" using the underlying sinon stub like the following:

client = ImportMock.mockClass(clientMod, "Client");
let stub = client.mock("someProp");
stub.get(() => {
    return { someFn: assertStub };
});

Unfortunately this also fails. The error message here is "Object.defineProperty called on non-object". I think my usecase should be covered by MockManager.replace directly, but I am not certain. Happy to build and PR an implementation, if my diagnosis is correct.

Vogel612 commented 3 years ago

I was able to make due with by writing my own "adapter" for a partial replacement and dropping that into set, but that feels like something this library should support out of the box... Currently it looks something like this:

class PropFake impements Prop {
    let replacement: Partial<Prop>;
    constructor(replacement: Partial<Prop>) {
        super();
        this.replacement = replacement;
    }
    someFn(args...) {
        if (typeof this.replacement.someFn !== "undefined") {
            this.replacement.someFn(args...);
        }
        super.someFn(args...);
    }
}

The obvious disadvantage here is that I haven't yet dealt with all the functions on the Class I originally wanted to mock. Adding prototype-based rewriting on top of sinon to mock an object feels wrong, though...

I don't think it's in the spirit of set to accept a Partial<T[K]>, but I don't have smart suggestions on solving that issue :/ Opinions?

EmandM commented 3 years ago

set should work fine for this use case. You should be able to do

client = ImportMock.mockClass(clientMod, "Client");
const assertStub = stub();
client.set("someProp", { someFunc: assertStub }); 

Please provide more details if there's an issue with this approach

Vogel612 commented 3 years ago

The issue I have when using set with a partial object is that the compiler (correctly) errors out on missing properties:

Argument of type '{ someFunc: Sinon.SinonStub<any[], any>; }' is not assignable to parameter of type 'Prop'.
  Type '{ someFunc: SinonStub<any[], any>; }' is missing the following properties from type 'Prop': [....]

Now if set accepted a Partial<T[K]>, this would not happen. I hope that clarifies the issue :)

EmandM commented 3 years ago

Ah yes, that is an issue. Set should accept Partial<T[K]>. Updating with a new build. Thanks for reporting this!