Skip to content

Prefer declared component instead of same component on imported module #288

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

Closed
marcioggs opened this issue Mar 10, 2022 · 9 comments
Closed

Comments

@marcioggs
Copy link

Hi!
First of all, thanks for your hard work simplificating my tests and I hope this becomes the de-facto library for Angular tests (as its name almost suggests it)✌

When we provide a service that's already contained in one of the imorted modules, Angular Testing Library gives preference to the one on the providers array. 👍

import { MockProvider} from 'ng-mocks';

await render(ComponentToTest, {
      imports: [ModuleContainingServiceAbc],
      providers: [MockProvider(ServiceAbc)] // Succeeds to use this one
    });

I expected the same to happen on the declarations array, but this is not happening.
Is this the implemented behavior? I'm not sure if I may be doing something wrong.

import { MockComponent } from 'ng-mocks';

await render(ComponentToTest, {
      imports: [ModuleContainingComponentAbc], 
      declarations: [MockComponent(ComponentAbc)] // Fails to use this one
    });

On my specific case, ComponentAbc is not directly declared and exported by ModuleContainingComponentAbc, but by another module that's imported by ModuleContainingComponentAbc.

If you need details on how ng-mocks creates the mocks:
https://ng-mocks.sudo.eu/api/MockProvider
https://ng-mocks.sudo.eu/api/MockComponent

@timdeschryver
Copy link
Member

I'm not familiar with ng-mocks, what's the behavior when you do this with a "normal" test that's using the Angular TestBed?

@marcioggs
Copy link
Author

marcioggs commented Mar 14, 2022

Hi, @timdeschryver.
Even without ng-mocks the problem persists, so it's not related to the way it creates the mock component.

Below you'll find standalone tests both using Testing Library and TestBed.
Both fails, using the real component (RealChildComponent) instead of the mocked one (MockedChildComponent).
This means that Testing Library is fine as it is, right?

import { render, screen } from '@testing-library/angular';
import { Component, NgModule } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';

@Component({
  selector: 'app-parent-component',
  template: '<app-child-component></app-child-component>',
})
class RealParentComponent {}

@Component({
  selector: 'app-child-component',
  template: '<p>Real child component</p>',
})
class RealChildComponent {}

@Component({
  selector: 'app-child-component',
  template: '<p>Mocked child component</p>',
})
class MockedChildComponent {}

@NgModule({
  declarations: [RealParentComponent, RealChildComponent],
})
export class RealModule {}

fdescribe('RealParentComponent', () => {
  it('Tests using Angular testing library', async () => {
    await render(RealParentComponent, {
      imports: [RealModule],
      declarations: [MockedChildComponent],
      excludeComponentDeclaration: true,
    });

    expect(screen.queryByText('Real child component')).toBeFalsy(); // Error: Expected <p>...</p> to be falsy
    expect(screen.queryByText('Mocked child component')).toBeTruthy(); // Error: Expected null to be truthy
  });

  it("Tests using Angular's TestBed", async () => {
    TestBed.configureTestingModule({
      imports: [RealModule],
      declarations: [MockedChildComponent],
    });

    const fixture: ComponentFixture<RealParentComponent> = TestBed.createComponent(RealParentComponent);

    expect(fixture.nativeElement.querySelector('p').textContent).not.toEqual('Real child component'); // Error: Expected 'Real child component' not to equal 'Real child component'.
    expect(fixture.nativeElement.querySelector('p').textContent).toEqual('Mocked child component'); // Expected 'Real child component' to equal 'Mocked child component'.
  });
});

The TestBed test fails to find app-child-component if RealChildComponent is removed from the module, which means that the mocked declaration on the test bed is being ignored:

@NgModule({
  declarations: [RealParentComponent /*, RealChildComponent */],
})
export class RealModule {}
ERROR: 'NG0304: 'app-child-component' is not a known element:
1. If 'app-child-component' is an Angular component, then verify that it is part of this module.
2. If 'app-child-component' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.'

The TestBed test succeeds if both components are declared directly on it (instead of on the module):

    TestBed.configureTestingModule({
      declarations: [RealParentComponent, MockedChildComponent],
    });

Conclusion:
It seems that TestBed ignores the declarations if imports is declared.

@timdeschryver
Copy link
Member

@marcioggs correct, that was my understanding as well the last time that I checked this but I wanted to be sure about it.
AFAIK we can't currently intervene this behavior.

@marcioggs
Copy link
Author

Maybe it's still possible to dynamically create a module that overrides the components in the module array with the components declared in the declarations array.
However, the providers array already works like this, so maybe this is a valid argument to request the implementation directly on Angular.

@marcioggs
Copy link
Author

Hi again, @timdeschryver .

Through this issue, I discovered that we can use TestBed#overrideComponent to achieve this.

Would Testing Library need to be changed to use this TestBed's method?
I didn't find any way of achieving this with the RenderResult object.

If it needs change and it makes sense to proceed with it, here's an interface spec alternative:

render(ComponentName, {
      imports: [...],
      providers: [...],
      overrideComponents: [ // {component: Type<any>, replacement: MetadataOverride<Component>}[]
        {
          component: ActivatedRoute,
          replacement: {
            set: { selector: 'app-child-component', template: '<p>Mocked child component</p>' },
          },
        },
        // Other replacements here
      ],
    })

There are also other override methods on TestBed that could make sense doing the same (overrideModule, overrideDirective, overridePipe and overrideProvider).

@satanTime
Copy link

Hi @marcioggs,

in theory "exclude" option of ng-mocks should help you here:

const dependencies = ngMocks.guts(null, ModuleContainingServiceAbc, ComponentToTest);
await render(ComponentToTest, dependencies);

There is an article about spectator, however, the same should be true for angular-testing-library: https://ng-mocks.sudo.eu/extra/with-3rd-party.

@timdeschryver
Copy link
Member

Thanks for the tip @satanTime .
Does that work for you @marcioggs ?
I'm actually not that motivated to add the override components into ATL because I'm not a big fan of shallow renders.

@marcioggs
Copy link
Author

marcioggs commented Mar 23, 2022

Hi. Sorry for the delay.
The example below works using ngMocks.guts. Thanks @satanTime 👍

import { Component, NgModule } from '@angular/core';
import { render, screen } from '@testing-library/angular';
import { ngMocks } from 'ng-mocks';

@Component({
  selector: 'app-parent-component',
  template: '<app-child-component></app-child-component>',
})
class RealParentComponent {}

@Component({
  selector: 'app-child-component',
  template: '<p>Real child component</p>',
})
class RealChildComponent {}

@NgModule({
  declarations: [RealParentComponent, RealChildComponent],
})
export class RealModule {}

fdescribe('RealParentComponent', () => {
  it('Tests using Angular testing library', async () => {
    const dependencies = ngMocks.guts(null, RealModule, RealParentComponent);

    await render(RealParentComponent, dependencies);

    expect(screen.queryByText('Real child component')).toBeNull();
  });
});

@timdeschryver
not being a fan of shallow rendering, what do you normally do instead?
On my case the parent component has references to N child components, and these child components calls web-services on ngOnInit, so I wanted to avoid having to create mocks for each of these services.
Thanks for the support!

@timdeschryver
Copy link
Member

I'm currently on my phone, feel free to add this example in the docs, or as an example in this repo. If you want to do this you can reopen this issue so we don't forget about it.
We can discuss the details later.

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

3 participants