angular2 test, how do I mock sub component

Unit TestingTestingJasmineAngularComponents

Unit Testing Problem Overview


How do I mock sub component in jasmine tests?

I have MyComponent, which uses MyNavbarComponent and MyToolbarComponent

import {Component} from 'angular2/core';
import {MyNavbarComponent} from './my-navbar.component';
import {MyToolbarComponent} from './my-toolbar.component';

@Component({
  selector: 'my-app',
  template: `
    <my-toolbar></my-toolbar>
    {{foo}}
    <my-navbar></my-navbar>
  `,
  directives: [MyNavbarComponent, MyToolbarComponent]
})
export class MyComponent {}

When I test this component, I do not want to load and test those two sub components; MyNavbarComponent, MyToolbarComponent, so I want to mock it.

I know how to mock with services using provide(MyService, useClass(...)), but I have no idea how to mock directives; components;

  beforeEach(() => {
    setBaseTestProviders(
      TEST_BROWSER_PLATFORM_PROVIDERS,
      TEST_BROWSER_APPLICATION_PROVIDERS
    );
    
    //TODO: want to mock unnecessary directives for this component test
    // which are MyNavbarComponent and MyToolbarComponent
  })
  
  it('should bind to {{foo}}', injectAsync([TestComponentBuilder], (tcb) => {
    return tcb.createAsync(MyComponent).then((fixture) => {
      let DOM = fixture.nativeElement;
      let myComponent = fixture.componentInstance;
      myComponent.foo = 'FOO';
      fixture.detectChanges();
      expect(DOM.innerHTML).toMatch('FOO');
    });
  });

Here is my plunker example;

http://plnkr.co/edit/q1l1y8?p=preview

Unit Testing Solutions


Solution 1 - Unit Testing

As requested, I'm posting another answer about how to mock sub components with input/output:

So Lets start by saying we have TaskListComponent that displays tasks, and refreshes whenever one of them is clicked:

<div id="task-list">
  <div *ngFor="let task of (tasks$ | async)">
    <app-task [task]="task" (click)="refresh()"></app-task>
  </div>
</div>

app-task is a sub component with the [task] input and the (click) output.

Ok great, now we want to write tests for my TaskListComponent and of course we don't want to test the real app-taskcomponent.

so as @Klas suggested we can configure our TestModule with:

schemas: [CUSTOM_ELEMENTS_SCHEMA]

We might not get any errors at either build or runtime, but we won't be able to test much other than the existence of the sub component.

So how can we mock sub components?

First we'll define a mock directive for our sub component (same selector):

@Directive({
  selector: 'app-task'
})
class MockTaskDirective {
  @Input('task')
  public task: ITask;
  @Output('click')
  public clickEmitter = new EventEmitter<void>();
}

Now we'll declare it in the testing module:

let fixture : ComponentFixture<TaskListComponent>;
let cmp : TaskListComponent;

beforeEach(() => {
  TestBed.configureTestingModule({
    declarations: [TaskListComponent, **MockTaskDirective**],
    // schemas: [CUSTOM_ELEMENTS_SCHEMA],
    providers: [
      {
        provide: TasksService,
        useClass: MockService
      }
    ]
  });

  fixture = TestBed.createComponent(TaskListComponent);
  **fixture.autoDetectChanges();**
  cmp = fixture.componentInstance;
});
  • Notice that because the generation of sub component of the fixture is happening asynchronously after its creation, we activate its autoDetectChanges feature.

In our tests, we can now query for the directive, access its DebugElement's injector, and get our mock directive instance through it:

import { By } from '@angular/platform-browser';    
const mockTaskEl = fixture.debugElement.query(By.directive(MockTaskDirective));
const mockTaskCmp = mockTaskEl.injector.get(MockTaskDirective) as MockTaskDirective;

[This part should usually be in the beforeEach section, for cleaner code.]

From here, the tests are a piece of cake :)

it('should contain task component', ()=> {
  // Arrange.
  const mockTaskEl = fixture.debugElement.query(By.directive(MockTaskDirective));

  // Assert.
  expect(mockTaskEl).toBeTruthy();
});

it('should pass down task object', ()=>{
  // Arrange.
  const mockTaskEl = fixture.debugElement.query(By.directive(MockTaskDirective));
  const mockTaskCmp = mockTaskEl.injector.get(MockTaskDirective) as MockTaskDirective;

  // Assert.
  expect(mockTaskCmp.task).toBeTruthy();
  expect(mockTaskCmp.task.name).toBe('1');
});

it('should refresh when task is clicked', ()=> {
  // Arrange
  spyOn(cmp, 'refresh');
  const mockTaskEl = fixture.debugElement.query(By.directive(MockTaskDirective));
  const mockTaskCmp = mockTaskEl.injector.get(MockTaskDirective) as MockTaskDirective;

  // Act.
  mockTaskCmp.clickEmitter.emit();

  // Assert.
  expect(cmp.refresh).toHaveBeenCalled();
});

Solution 2 - Unit Testing

If you use schemas: [CUSTOM_ELEMENTS_SCHEMA]in TestBed the component under test will not load sub components.

import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed, async } from '@angular/core/testing';
import { MyComponent } from './my.component';

describe('App', () => {
  beforeEach(() => {
    TestBed
      .configureTestingModule({
        declarations: [
          MyComponent
        ],
        schemas: [CUSTOM_ELEMENTS_SCHEMA]
      });
  });

  it(`should have as title 'app works!'`, async(() => {
    let fixture = TestBed.createComponent(MyComponent);
    let app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('Todo List');
  }));

});

This works in the released version of Angular 2.0. Full code sample here.

An alternative to CUSTOM_ELEMENTS_SCHEMA is NO_ERRORS_SCHEMA

Solution 3 - Unit Testing

Thanks to Eric Martinez, I found this solution.

We can use overrideDirective function which is documented here, https://angular.io/docs/ts/latest/api/testing/TestComponentBuilder-class.html

It takes three prarmeters;

  1. Component to implement
  2. Child component to override
  3. Mock component

Resolved solution is here at http://plnkr.co/edit/a71wxC?p=preview

This is the code example from the plunker

import {MyNavbarComponent} from '../src/my-navbar.component';
import {MyToolbarComponent} from '../src/my-toolbar.component';

@Component({template:''})
class EmptyComponent{}

describe('MyComponent', () => {

  beforeEach(injectAsync([TestComponentBuilder], (tcb) => {
    return tcb
      .overrideDirective(MyComponent, MyNavbarComponent, EmptyComponent)
      .overrideDirective(MyComponent, MyToolbarComponent, EmptyComponent)
      .createAsync(MyComponent)
      .then((componentFixture: ComponentFixture) => {
        this.fixture = componentFixture;
      });
  ));
  
  it('should bind to {{foo}}', () => {
    let el = this.fixture.nativeElement;
    let myComponent = this.fixture.componentInstance;
    myComponent.foo = 'FOO';
    fixture.detectChanges();
    expect(el.innerHTML).toMatch('FOO');    
  });
});

Solution 4 - Unit Testing

I put together a simple MockComponent module to help make this a little easier:

import { TestBed } from '@angular/core/testing';
import { MyComponent } from './src/my.component';
import { MockComponent } from 'ng2-mock-component';
 
describe('MyComponent', () => {
 
  beforeEach(() => {

    TestBed.configureTestingModule({
      declarations: [
        MyComponent,
        MockComponent({ 
          selector: 'my-subcomponent', 
          inputs: ['someInput'], 
          outputs: [ 'someOutput' ]
        })
      ]
    });
 
    let fixture = TestBed.createComponent(MyComponent);
    ...
  });
 
  ...
});

It's available at https://www.npmjs.com/package/ng2-mock-component.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionallenhwkimView Question on Stackoverflow
Solution 1 - Unit TestingbaryoView Answer on Stackoverflow
Solution 2 - Unit TestingKlas MellbournView Answer on Stackoverflow
Solution 3 - Unit TestingallenhwkimView Answer on Stackoverflow
Solution 4 - Unit TestingChristian NunciatoView Answer on Stackoverflow