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

Cannot test @Injectable() services with Jest #54

Closed
PabloDotcom opened this issue Feb 13, 2020 · 22 comments
Closed

Cannot test @Injectable() services with Jest #54

PabloDotcom opened this issue Feb 13, 2020 · 22 comments

Comments

@PabloDotcom
Copy link

I've done migration from ngx-take-until-destroy to ngneat/until-destroy (after migration to angular packages in ver. 9.0.0). As application itself works fine, I've got issues with jest unit tests.

Error:
TypeError: Cannot read property 'onDestroy' of undefined

export function isInjectableType(target: any): target is InjectableType<unknown> {

It's because you depend on target.ɵprov, but it doesn't have to be added by ivy compiler.

@NetanelBasal
Copy link
Member

Can you reproduce it, please?

@PabloDotcom
Copy link
Author

Sure @NetanelBasal. Jest is setup with 'jest-preset-angular' ver. 8.0.0.
package.json command: jest --config jest.config.js

Failed spec:

`import { CtdPopupLoginComponent } from 'layouts/ctdPopup/CtdPopupLoginComponent';
import { of } from 'rxjs';
import { createMockInstance } from 'setup/spyHelper';
import { EnvironmentService } from 'shared/services/abstract/environment/EnvironmentService';
import { UserService } from 'shared/services/abstract/user/UserService';

describe('CtdPopupLoginComponent', () => {
let component: CtdPopupLoginComponent;
let userServiceMock: jest.Mocked;
let environmentServiceMock: jest.Mocked;
let user: any;

beforeEach(() => {
jest.clearAllMocks();

user = {
    name: 'test name',
};

userServiceMock = createMockInstance(UserService, {
    user$: of(user),
});

environmentServiceMock = createMockInstance(EnvironmentService, {
    environmentConfig: {
        dashboard: 'https://test.api.com',
    } as any,
});

component = new CtdPopupLoginComponent(
    userServiceMock,
    environmentServiceMock,
);

});

it('should be a defined component', () => {
expect(component).toBeTruthy();
});

it('should open new window with params', () => {
window.open = jest.fn();
component.goToLoginPage();
expect(window.open).toBeCalled();
const params = 'height=600,width=800,top=-300,
left=-400';
expect(window.open).toBeCalledWith('https://test.api.com', 'User login page', params);
});
});`

Error:
`Test suite failed to run

TypeError: Cannot read property 'onDestroy' of undefined

3 | import { tap } from 'rxjs/operators';
4 | import { EnvironmentService } from 'shared/services/abstract/environment/EnvironmentService';

5 | import { UserService } from 'shared/services/abstract/user/UserService';
| ^
6 |
7 | @UntilDestroy()
8 | @component({

at decorateDirective (node_modules/@ngneat/until-destroy/bundles/ngneat-until-destroy.umd.js:95:49)
at node_modules/@ngneat/until-destroy/bundles/ngneat-until-destroy.umd.js:105:17
at Object..__decorate (src/app/layouts/ctdPopup/CtdPopupLoginComponent.ts:5:95)
at Object. (src/app/layouts/ctdPopup/CtdPopupLoginComponent.ts:12:36)
at Object. (test/unit/platforms/ctd-extension/popup/CtdPopupLoginComponent.spec.ts:1:1)`
I guess, it's because component is created without TestBed (it's not a good arch of tests, but it worked until now and we have a lot of legacy in unit tests now, so it's not that easy to improve them that fast). Anyway the quick fix for that is:

jest.mock('@ngneat/until-destroy', () => ({ ...jest.requireActual('@ngneat/until-destroy'), UntilDestroy: jest.fn().mockImplementation((options: any = {}) => { return (target: any) => { if (!target.ɵprov) { target.ɵprov = true; } return jest.requireActual('@ngneat/until-destroy').UntilDestroy(options)(target); }; }), }));

Maybe there is a better way of checking Injectable than depending on target.ɵprov (injected by ivy compiler)?

@NetanelBasal
Copy link
Member

Why it's a @component and not @Component?

@PabloDotcom
Copy link
Author

PabloDotcom commented Feb 14, 2020

It was formatted when I've pasted that. Its @Component in source code and in error.
Screen Shot 2020-02-14 at 4 24 36 PM

Sorry for that.

In source code:

`import { Component, OnDestroy } from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { tap } from 'rxjs/operators';
import { EnvironmentService } from 'shared/services/abstract/environment/EnvironmentService';
import { UserService } from 'shared/services/abstract/user/UserService';

@UntilDestroy()
@component({
selector: 'ctd-popup-login',
templateUrl: 'ctd-popup-login.html',
})
export class CtdPopupLoginComponent implements OnDestroy {`

Those services for tests are created with:
import createMockInstance1 from 'jest-create-mock-instance';

It's obviously problem with Jest environment, but it's because of usage of ivy flags in 'take-until'.
Probably more devs will have problem with that in other runtiime envs too, I guess.

@NetanelBasal
Copy link
Member

We test the project with Jest, both the playground and the library, and it works fine.

@brianmcd
Copy link

I'm also seeing this with Jest and a service provided at the component level after upgrading from Angular 8 and ngx-take-until-destroy to @ngneat/until-destroy and Angular 9. I'll see if I can create a repro tomorrow.

@arturovt
Copy link
Collaborator

Gentlemen, that would be awesome if you could create a minimal reproducible example for us.

@brianmcd
Copy link

@arturovt @NetanelBasal - Here is a repro repository: https://github.com/brianmcd/until-destroy-bug

What I'm seeing is that if you provide the ExampleService in a Component or Module's providers array, then the test in that repository will fail with TypeError: Cannot read property 'onDestroy' of undefined. If you provide the ExampleService via providedIn: 'root' or providedIn: 'any', then the test passes.

@arturovt
Copy link
Collaborator

Thank you, Brian! I will try to have a look during the week when I get some free time!

@brianmcd
Copy link

brianmcd commented Feb 17, 2020

No problem. Thanks for looking into it, Artur. I should mention that I generated the repro project with Nx, since that's an easy way to get an app running with Jest. My real project that's hitting this bug is also using Nx. I don't think that should make a difference, but wanted to mention it just in case.

@PabloDotcom
Copy link
Author

Thanks @brianmcd. @arturovt I don't use nwrl nx in project I've got the error.

@arturovt
Copy link
Collaborator

arturovt commented Feb 17, 2020

@brianmcd

I was able to reproduce your problem. The issue is here:

ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points

Jest uses the CJS module system and those bundles are provided via UMD.

Before making any changes you can put this into app.component.spec.ts:

import { ɵivyEnabled as ivyEnabled } from '@angular/core';
console.log(`ivyEnabled = ${ivyEnabled}`);
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';

You will see that ivyEnabled = false in your test environment.

You have to remove all flags because Angular Ivy guide mentions that:

Don't use --create-ivy-entry-points as this will cause Node not to resolve the Ivy version of the packages correctly.

Also --first-only is not needed, from its description:

If specified then only the first matching
package.json property will be compiled

So basically that seems weird that you pass these options es2015 browser module main with the --first-only since es2015 will be taken.

So when I ran plain ngcc command w/o anything and then yarn ng test --watch I received that output:
image

@brianmcd
Copy link

Thanks so much for looking into this, @arturovt. I changed the postinstall to just run ngcc and things are working now.

The postinstall command ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points was added automatically by Nx, but is also mentioned here. Like you said, the docs do later mention that the flags will cause issue with module resolution in Node.

From digging through issues, it looks like Nx adds the postinstall so that ngcc runs before anything else, which was required for parallel builds. From what I could gather, the Angular CLI team is working on introducing a lockfile that should remove the need for the postinstall script for Nx users.

The relevant issues are:

It's also worth noting that the standard Angular CLI doesn't add any postinstall step, and it seems to only build esm2015 versions, so I think users of the stock CLI who use Jest will see this behavior, too.

@Rush
Copy link

Rush commented Feb 17, 2020

Unfortunately in some cases --create-ivy-entry-points is needed, as per this ticket angular/angular#35000 (comment)

@arturovt
Copy link
Collaborator

Gentlemen, is it resolved? Can the issue be closed?

@brianmcd
Copy link

For me, everything is working with running ngcc with no flags in the postinstall step. Thanks again for your help, @arturovt.

@Plysepter
Copy link

I am not sure that this should be considered closed just yet. I've been experiencing this issue myself and running ngcc without any parameters does remove the TypeError: Cannot read property 'onDestroy' of undefined error but I instead encounter the following error when using jest's snapshot feature: PrettyFormatPluginError: Cannot read property 'element' of undefinedTypeError: Cannot read property 'element' of undefined

However if I remove the usage of this library from the code while using NX's default ngcc parameters, the tests all pass. I haven't been able to find the exact reasoning as to why snapshots of the fixture fail when the extra parameters have been added to the ngcc command / when ivy is enabled. However this conflict is preventing my tests from passing and I would greatly appreciate if anyone else might have other suggestions or insights into the matter.

I have forked @brianmcd's example repo here and have added a few branches demonstrating the different variations which can be tested by removing and reinstalling the node_modules folder to recompile with ngcc followed by running the tests ng t. Master is the base that has already been tested but updated with a snapshot test. The other branches are variations of the master branch but either with the ngcc parameters removed or this library commented out.

If anyone can help resolve this, I would be extremely grateful!

@kremerd
Copy link

kremerd commented Feb 25, 2020

I had this issue as well. Changing the postinstall script as suggested by @arturovt solved the primary error described above, but also introduced a new, i18n-related error:

It looks like your application or one of its dependencies is using i18n.
Angular 9 introduced a global `$localize()` function that needs to be loaded.
Please run `ng add @angular/localize` from the Angular CLI.
(For non-CLI projects, add `import '@angular/localize/init';` to your `polyfills.ts` file.
For server-side rendering applications add the import to your `main.server.ts` file.)

In total my tests kept on failing. Fortunately, the solution for this problem was more straight forward: You have to add the line

setupFiles: ['@angular/localize/init']

to the projects test configuration file jest.config.js.

In case that's not clear enough, here's a small bash script to get a minimal example for this scenario up and running:

# Set up a simple nx workspace
npx create-nx-workspace@latest demo
? What to create in the new workspace angular
? Application name demo
? Default stylesheet format CSS
cd demo

# Add some i18n
sed "s/<h1>/<h1 i18n>/" -i apps/demo/src/app/app.component.html
ng add @angular/localize

# Install and use `until-destroy`
npm install @ngneat/until-destroy
sed -e "2i import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';" \
  -e "2i import { of } from 'rxjs';" \
  -e "3i @UntilDestroy()" \
  -e "10i obs$ = of().pipe(untilDestroyed(this));" \
  -i apps/demo/src/app/app.component.ts

# Update the `postinstall` script to `ngcc`
sed "s/ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points/ngcc/" -i package.json
rm -rf node_modules
npm install

# Update Jest config
sed "5i setupFiles: ['./jest.setup.ts']," -i apps/demo/jest.config.js
echo "import '@angular/localize/init';" > apps/demo/jest.setup.ts

# Tests now pass again
ng test

@Plysepter
Copy link

As an update on my report: It looks like there are compatibility issues within jest-preset-angular and ivy in the serializer. I believe (at least in the minimum reproduction instance) that running ngcc without any parameters will resolve my use case once thymikee/jest-preset-angular#351 is resolved

@arturovt
Copy link
Collaborator

arturovt commented Mar 31, 2020

@Plysepter I have fixed this issue in this PR. @8.1.3 version has been published there.

@Plysepter
Copy link

Thank you @arturovt, it is greatly appreciated!

@NetanelBasal
Copy link
Member

I'm closing this issue. Please re-open with a reproduction if the problem still exists. Thanks.

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

No branches or pull requests

7 participants