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]: Exclude ɵfac ɵprov properties from auto mock #2908

Open
MillerSvt opened this issue Dec 27, 2024 · 8 comments · May be fixed by #2953
Open

[Feature]: Exclude ɵfac ɵprov properties from auto mock #2908

MillerSvt opened this issue Dec 27, 2024 · 8 comments · May be fixed by #2953

Comments

@MillerSvt
Copy link

MillerSvt commented Dec 27, 2024

🚀 Feature Proposal

Exclude ɵfac ɵprov properties from jest auto mock

Motivation

Currently, I'm trying to test a component that depends on an external Angular library. This library provides a SomeService that is provided in root.

Here is my component:

class Component {
  someService = inject(SomeService);

  method() {
    this.someService.someMethod();
  }
}

Here is my test:

import { SomeService } from 'some-library';

jest.mock('some-library');

beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [SomeService],
  });

  fixture = TestBed.createComponent(Component);
  component = fixture.componentInstance;
  fixture.detectChanges();
});

it('test', () => {
  component.method();
});

Currently I receive error: Cannot read property 'someMethod' of undefined.

This happens because of jest auto mocks, mocking ɵfac and ɵprov as well. Is it possible to exclude them?

Example

I've done some research. Here is my working patch:

jest.mock(`some-library`, () => {
    const moduleMock = jest.createMockFromModule(`some-library`);

    function* walk(obj: unknown, walkedNodes: any[] = []): Generator<[key: string, target: any]> {
        if ((typeof obj !== `function` && typeof obj !== `object`) || walkedNodes.includes(obj)) {
            return;
        }

        for (const key in obj) {
            if (typeof key === `string` && key.startsWith(`ɵ`)) {
                yield [key, obj];
            }

            yield* walk(obj[key], [...walkedNodes, obj]);
        }
    }

    for (const [key, target] of walk(moduleMock)) {
        switch (key) {
            case `ɵfac`: {
                target[key] = () => new target();
                break;
            }
            case `ɵprov`: {
                if (target[key] === undefined) {
                    break;
                }

                if (`factory` in target[key]) {
                    target[key].factory = () => new target();
                }

                break;
            }
        }
    }

    return moduleMock;
});

However, it would not be beneficial to repeat it in each test and mock. Perhaps it could be placed there?

@ahnpnl
Copy link
Collaborator

ahnpnl commented Dec 27, 2024

Would it work if you do like this

import { SomeService } from 'some-library';

jest.mock('some-library');

const FakeSomeServiceInstance = {
     someMethod: jest.fn(),
}

beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [{
       provide: SomeService,
       useValue: FakeSomeServiceInstance,
   }],
  });

  fixture = TestBed.createComponent(Component);
  component = fixture.componentInstance;
  fixture.detectChanges();
});

it('test', () => {
  component.method();

  expect(FakeSomeServiceInstance.someMethod).toHaveBeenCalled();
});

or like this

import { SomeService } from 'some-library';

jest.mock('some-library', () => {
    // write mock implementation for `some-library` here to make sure it returns a `SomeService` fake instance
});

beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [SomeService],
  });

  fixture = TestBed.createComponent(Component);
  component = fixture.componentInstance;
  fixture.detectChanges();
});

it('test', () => {
  component.method();
});

In general, jest-preset-angular can't interfere with how Jest mocking mechanism works behind the scene, and I think we shouldn't do that either. TestBed already provided a good way to mock the DI, we should leverage that utility. jest.mock is also powerful enough to achieve what we need.

@MillerSvt
Copy link
Author

Would it work if you do like this

Yes, both options work. But I would like to somehow unify the behavior when the service is mocked from the project, and from the library. If you mock the service from the project, then everything works fine, but for the service from the library, you have to write some extra stuff. This increases the learning curve of new employees.

Therefore, if jest does not offer this capability and it is not applicable at the jest-preset-angular level, I will attempt to submit a feature request to jest.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Dec 27, 2024

You can also create testing utilities which provides the reusable ways to mock things, document and that would also help to instruct new employees too :)

@MillerSvt
Copy link
Author

MillerSvt commented Dec 27, 2024

You can also create testing utilities

No, I can't. That would be too easy. The fact is that a call to the jest.mock function is parsed statically. And there is no way to pass dynamic parameters there, such as utility functions or constants from outside.

The only way to duplicate this factory function in each test in each jest.mock call.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Dec 27, 2024

Currently, automock is a global config option while jest.mock provides the possibility to configure mocking for specific things. I'm not sure how we let Jest know which properties to exclude from automock.

In Vitest, they have an API like this https://vitest.dev/api/vi#vi-hoisted which I think can allow to pass things from outside which Jest doesn't have. Maybe you can look at Vitest API to come up with a nice proposal for Jest since Jest and Vitest are kind of identical

@MillerSvt
Copy link
Author

If jest approved the suggested API, could you host a mock transformer here?

It should be something like:

jest.onGenerateMock((moduleName, moduleMock) => {
  // some modifications for moduleMock (as described in example)
  return moduleMock;
});

@ahnpnl
Copy link
Collaborator

ahnpnl commented Dec 30, 2024

Ye, it should be possible to add to the list of hoisting methods

@MillerSvt
Copy link
Author

MillerSvt commented Jan 15, 2025

Pull request is merged. Waiting for release...

MillerSvt pushed a commit to MillerSvt/jest-preset-angular that referenced this issue Jan 30, 2025
MillerSvt added a commit to MillerSvt/jest-preset-angular that referenced this issue Jan 30, 2025
@MillerSvt MillerSvt linked a pull request Jan 30, 2025 that will close this issue
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants