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

Feature Request: allow to mock only specific methods #4367

Closed
serhiistotskyi opened this issue Nov 29, 2022 · 4 comments · Fixed by #4502
Closed

Feature Request: allow to mock only specific methods #4367

serhiistotskyi opened this issue Nov 29, 2022 · 4 comments · Fixed by #4502
Assignees
Labels

Comments

@serhiistotskyi
Copy link

Describe the feature or problem you'd like to solve

Sometimes I want to mock just one method and leave the rest of the service as is. So, it can run some domain specific logic (or side-effects)

Proposed solution

Provide a function that has the same declaration as MockProvider but modifies service instead of creating a full mock:

class MyService {
  someMethod() {
       console.log('implementation')
       return of(undefined);
  }

  anotherMethod() {
        return this.someMethod().pipe(
          tap(() => {
              // side effects, changes to service properties or changes in dependencies
          })
        )
  }
}

PatchProvider(MyService, {
  someMethod: jest.fn(() => of(...))
});

// later in tests
const service = TestBed.inject(MyService);

console.log(service.someMethod) // jest.fn()
console.log(service.anotherMethod) // original method

Additionally would be nice to ignore undefined values in object, i.e., if property is undefined just ignore it and do not override service method/property:

PatchProvider(MyService, {
  someMethod: jest.fn(() => of(...)),
  anotherMethod: undefined // ignored, as if there is no this property at all
});

this allows to simplify pattern of using setup function and passing props inside:

function setup(props?: Props) {
   TestBed.configureTestingModule({
       providers: [
           PatchProvider(MyService, {
              someMethod: props?.someMethod
           })
       ]
    });
}

interface Props {
  someMethod?: MyService["someMethod"]
}

We use this pattern a lot in our codebase, together with MockProvider, so would be nice to have this built-in.

Additional context

We have implemented this custom function using APP_INITIALIZER token:

function PatchProvider<T extends {}>(
    token: ProviderToken<T>,
    modifications: Partial<T>
): FactoryProvider {
  return {
        provide: APP_INITIALIZER,
        multi: true,
        deps: [token],
        useFactory(service: T) {
            return () =>
                Object.keys(modifications).forEach((key) => {
                    const value = (modifications as Record<string, unknown>)[key];
                    if (value === undefined) return;

                    (service as Record<string, unknown>)[key] = value;
                });
        },
    };
}
@satanTime
Copy link
Member

satanTime commented Dec 10, 2022

Hi @serhiistotskyi,

thank you for the request.

I've fixed the part with undefined. it will be merged soon.

Regarding partial mocks, it's bad practice and isn't supported by ng-mocks.
Could you give me an example when you need to mock a service partially?

satanTime added a commit to satanTime/ng-mocks that referenced this issue Dec 11, 2022
satanTime added a commit that referenced this issue Dec 11, 2022
feat(MockInstance): ignores undefined properties #4367
@satanTime
Copy link
Member

v14.5.0 has been released and contains a fix for the issue. Feel free to reopen the issue or to submit a new one if you meet any problems.

@stalniy
Copy link

stalniy commented Dec 16, 2022

Why is it a bad practice? I’ll look into our use cases and will let you know. But I had issues with Router and routerLink when I tried to completely mock router -> routerLink directive threw an error

@satanTime
Copy link
Member

satanTime commented Dec 17, 2022

Basically, in unit tests you have a subject under test and the subject should stay as it is, and its dependencies should be mocked.
if you need to mock a part of the subject (unit) - it means bad design of the subject, and maybe the part you want to mock should be an external dependency.

If you have integration tests, then you have 2+ subjects under test and it's the same story - their dependencies should be mocked, but the subjects should stay as they are.

Regarding the error when you mock, I think, it's because not all dependencies have been mocked, especially routerLink, otherwise it would be an empty mock which doesn't have logic to throw errors.
An example how a mock RouterModule works well with routerLink: https://codesandbox.io/s/boring-black-20jj5r?file=/src/test.spec.ts:902-1177

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

Successfully merging a pull request may close this issue.

3 participants