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

Mock get/set properties when mocking classes #177

Closed
JoshuaEN opened this issue Aug 4, 2020 · 4 comments · Fixed by #180
Closed

Mock get/set properties when mocking classes #177

JoshuaEN opened this issue Aug 4, 2020 · 4 comments · Fixed by #180

Comments

@JoshuaEN
Copy link

JoshuaEN commented Aug 4, 2020

Hello,

For classes, MockService appears to only mock methods, deferring get/set properties to the original class's prototype.

At least for my use-case, it would be preferable if get/set properties were also mocked to prevent a unit test from accidentally using the actual implementation of a dependency.

Example of Current + Desired behavior (the test passing is desired):

class Example {
    get hardCodedGet() {
        return 'return value from hardCodedGet';
    }

    private _dynamicGet = 'return value from dynamicGet';
    get dynamicGet() {
        return this._dynamicGet;
    }

    hardCodedMethod() {
        return 'return value from hardCodedMethod';
    }

    private _dynamicMethod = 'return value from dynamicMethod';
    dynamicMethod() {
        return this._dynamicMethod;
    }
}

describe('ng-mocks MockService class mocking behavior', () => {
    it('should mock get/set properties and methods', () => {
        const mockedExample = MockService(Example);

        // Properties
        expect(mockedExample.hardCodedGet).toBeUndefined(); // Fails
        expect(mockedExample.dynamicGet).toBeUndefined(); // Passes
        mockedExample['_dynamicGet'] = 'value set in test to _dynamicGet';
        expect(mockedExample.dynamicGet).toBeUndefined(); // Fails because backing field was set in the line above

        // Methods
        expect(mockedExample.hardCodedMethod()).toBeUndefined(); // Passes
        expect(mockedExample.dynamicMethod()).toBeUndefined(); // Passes
        mockedExample['_dynamicMethod'] = 'value set in test to _dynamicMethod';
        expect(mockedExample.dynamicMethod()).toBeUndefined(); // Passes
    });
});

Example in a runnable project

Thoughts?


From a technical standpoint, this would be my proposed change:

Add (to createMockFromPrototype in mock-service.ts):

const properties = mockServiceHelperPrototype.extractPropertiesFromPrototype(service);
for (const prop of properties) {
  if (value[prop] || Object.getOwnPropertyDescriptor(value, prop)) {
    continue;
  }

  const descriptor = Object.getOwnPropertyDescriptor(service, prop);
  if (descriptor?.get) {
    mockServiceHelper.mock(value, prop, 'get');
  }
  if (descriptor?.set) {
    mockServiceHelper.mock(value, prop, 'set');
  }
}

I would be happy to submit a PR (with tests) if this change is acceptable.


P.S. I noted a few other places stub both the get and set property, even if only one was originally defined.

By stubbing just the get/set properties that were defined, the mock more accurately represents the original class, which I personally would prefer. That said, I'm happy to follow the other pattern here if is preferred (or update the other places to only mock what was originally there, if desired).

@satanTime
Copy link
Member

satanTime commented Aug 5, 2020

Hi,

thanks for the report.

that's the tricky part because not all get / set properties are visible via getOwnPropertyDescriptor, what can be extracted is mocked out of the box.

these properties are defined in js constructor like this.hardCodedGet = and when we mock the constructor we lose these declarations, an example can be found by the link below (TS 3.7).

as an option you can try to do next:

if you use TS 3.7: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier

or to mock them manually.

mockServiceHelper.mock(instance, 'hardCodedGet', 'get');
mockServiceHelper.mock(instance, 'dynamicGet', 'get');

@JoshuaEN
Copy link
Author

JoshuaEN commented Aug 5, 2020

Thank you for looking into this.

that's the tricky part because not all get / set properties are visible via getOwnPropertyDescriptor, what can be extracted is mocked out of the box.

Maybe I'm misunderstanding, but the issue I am experiencing is, when using MockService(Class), get/set properties defined on the class's prototype (visible to getOwnPropertyDescriptor) are not mocked:

    class Example {
      get val() { throw new Error('Not mocked'); }
    }

    console.log(mockServiceHelper.extractPropertiesFromPrototype(Example.prototype)); // ['val']
    
    const mockExample = MockService(Example);
    console.log(mockExample.val); // Throws "Not mocked"

these properties are defined in js constructor like this.hardCodedGet = and when we mock the constructor we lose these declarations, an example can be found by the link below (TS 3.7).

Sorry if my original post was not clear; my issue is not regarding fields , but rather get/set properties defined on the prototype.

@satanTime
Copy link
Member

satanTime commented Aug 8, 2020

Hi @JoshuaEN,

please excuse for the delay, might you check if the fix works for you: ng-mocks.zip?

satanTime referenced this issue in satanTime/ng-mocks Aug 8, 2020
@JoshuaEN
Copy link
Author

Hi @satanTime,

This fix works perfectly, thank you!

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 a pull request may close this issue.

2 participants