Mocking Child Components - Angular 2

How do you mock a child component when testing? I have a parent component called product-selected whose template looks like this:

<section id="selected-container" class="container-fluid">
    <hr/>
  <product-settings></product-settings>
  <product-editor></product-editor>
  <product-options></product-options>
</section>

And the component declaration looks like this:

import { Component, Input }               from '@angular/core'; 

import { ProductSettingsComponent } from '../settings/product-settings.component';                                      
import { ProductEditorComponent }   from '../editor/product-editor.component';                                      
import { ProductOptionsComponent }  from '../options/product-options.component';                                        

@Component({
    selector: 'product-selected',
    templateUrl: './product-selected.component.html',
    styleUrls: ['./product-selected.component.scss']
})
export class ProductSelectedComponent {}

This component is really just a place for the other components to live in and probably won't contain any other functions.

But when I set up the testing I get the following template error, repeated for all three components:

Error: Template parse errors:
    'product-editor' is not a known element:
    1. If 'product-editor' is an Angular component, then verify that it is part of this module.
    2. If 'product-editor' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message. ("
        <hr/>
      <product-settings></product-settings>
      [ERROR ->]<product-editor></product-editor>

I've tried to load a mocked version of the child components but don't know how to do - the examples that I've seen just override the parent and don't even mention the child components. So how do I go about doing it?


Solution 1:

Careful about the NO_ERRORS_SCHEMA. Let's quote another part of the same the docs:

Shallow component tests with NO_ERRORS_SCHEMA greatly simplify unit testing of complex templates. However, the compiler no longer alerts you to mistakes such as misspelled or misused components and directives.

I find that drawback quite contrary to the purposes of writing a test. Even more so that it's not that hard to mock a basic component.

An approach not yet mentioned here is simply to declare them at config time:

@Component({
  selector: 'product-settings',
  template: '<p>Mock Product Settings Component</p>'
})
class MockProductSettingsComponent {}

@Component({
  selector: 'product-editor',
  template: '<p>Mock Product Editor Component</p>'
})
class MockProductEditorComponent {}

...  // third one

beforeEach(() => {
  TestBed.configureTestingModule({
      declarations: [
        ProductSelectedComponent,
        MockProductSettingsComponent,
        MockProductEditorComponent,
        // ... third one
      ],
      providers: [/* your providers */]
  });
  // ... carry on
});

Solution 2:

Found a nearly perfect solution, that will also correctly throw errors if someone refactors a component: https://www.npmjs.com/package/ng-mocks

npm install ng-mocks --save-dev

Now in your .spec.ts you can do

import { MockComponent } from 'ng-mocks';
import { ChildComponent } from './child.component.ts';
// ...
beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [FormsModule, ReactiveFormsModule, RouterTestingModule],
    declarations: [
      ComponentUnderTest,
      MockComponent(ChildComponent), // <- here is the thing
      // ...
    ],
  });
});

This creates a new anonymous Component that has the same selector, @Input() and @Output() properties of the ChildComponent, but with no code attached.

Assume that your ChildComponent has a @Input() childValue: number, that is bound in your component under test, <app-child-component [childValue]="inputValue" />

Although it has been mocked, you can use By.directive(ChildComponent) in your tests, as well as By.css('app-child-component') like so

it('sets the right value on the child component', ()=> {
  component.inputValue=5;
  fixture.detectChanges();
  const element = fixture.debugElement.query(By.directive(ChildComponent));
  expect(element).toBeTruthy();

  const child: ChildComponent = element.componentInstance;
  expect(child.childValue).toBe(5);
});

Solution 3:

Generally, if you have a component used inside the view of the component that you're testing and you don't necessarily want to declare those components because they might have their own dependencies, in order to avoid the "something is not a known element" error you should use NO_ERRORS_SCHEMA.

import { NO_ERRORS_SCHEMA }          from '@angular/core';

TestBed.configureTestingModule({
        declarations: declarations,
        providers: providers
        schemas:      [ NO_ERRORS_SCHEMA ]
  })

Based on the docs :

Add NO_ERRORS_SCHEMA to the testing module's schemas metadata to tell the compiler to ignore unrecognized elements and attributes. You no longer have to declare irrelevant components and directives.

More on this : https://angular.io/docs/ts/latest/guide/testing.html#!#shallow-component-test

Solution 4:

I posted this question so I could post an answer as I struggled with this for a day or two. Here's how you do it:

let declarations = [
  ProductSelectedComponent,
  ProductSettingsComponent,
  ProductEditorComponent,
  ProductOptionsComponent
];

beforeEach(() => {
        TestBed.configureTestingModule({
            declarations: declarations,
            providers: providers
        })
        .overrideComponent(ProductSettingsComponent, {
            set: {
                selector: 'product-settings',
                template: `<h6>Product Settings</h6>`
            }
        })
        .overrideComponent(ProductEditorComponent, {
            set: {
                selector: 'product-editor',
                template: `<h6>Product Editor</h6>`
            }
        })
        .overrideComponent(ProductOptionsComponent, {
            set: {
                selector: 'product-options',
                template: `<h6>Product Options</h6>`
            }
        });

        fixture = TestBed.createComponent(ProductSelectedComponent);
        cmp = fixture.componentInstance;
        de = fixture.debugElement.query(By.css('section'));
        el = de.nativeElement;
    });

You have to chain the overrideComponent function for each of the child components.