67

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

Liam
  • 27,717
  • 28
  • 128
  • 190
allenhwkim
  • 27,270
  • 18
  • 89
  • 122
  • The components are working fine, your issue is another thing. You're importing, for example, `MyNavbarComponent` but in your component class is called `myNavbarComponent`. Note the lowercase `m`, that makes it fail. If you uppercase it it will work fine. – Eric Martinez Mar 13 '16 at 21:13
  • thanks @EricMartinez, I fixed lowercase and the test works. However my question is still valid with how to mock a component. I am testing `MyComponent`, not `MyNavbarComponent` nor `MyToolbarComponent` – allenhwkim Mar 13 '16 at 21:19
  • Yes, I'm sorry. You can take a look at this [spec](https://github.com/angular/angular/blob/9e44dd85ada181b11be869841da2c157b095ee07/modules/angular2/test/testing/test_component_builder_spec.ts#L152) and see how they mock the component. – Eric Martinez Mar 13 '16 at 21:20
  • @EricMartinez, thanks. I posted my own answer learned from your commnet. All credit goes to you. – allenhwkim Mar 14 '16 at 00:37

4 Answers4

74

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();
});
Liam
  • 27,717
  • 28
  • 128
  • 190
baryo
  • 1,441
  • 13
  • 18
  • You may want to have an example where `fixture.detectChanges()` is called. My app's sub-component's data was only set on a method call so in order to get this example working for me I had to call the method on the fixture and then call `detectChanges()` for it to work. – Kent Bull Nov 20 '16 at 22:25
  • 1
    Is By.directive something that needs to be imported separately to be used? - I'm getting an error in my Karma test saying: "ReferenceError: By is not defined" – Alex Dec 12 '16 at 14:41
  • 4
    @Alex I was wondering the same thing.. `import { By } from '@angular/platform-browser';` worked for me. – Benjamin Dec 13 '16 at 13:43
  • 1
    What a great answer. Saved me a lot of time! – Radosław Roszkowiak Jan 30 '17 at 17:36
  • 1
    This solution however wont solve the problem when parent use @ViewChild(SubComponent). Still waiting for Angular team to resolve this – Julian Jun 05 '17 at 09:37
  • can you link to the open issue? – baryo Jun 05 '17 at 10:13
  • @Nicolas, I just use local variables to solve the problem. `@ViewChild('my-sub-component')` works if you have `#my-sub-component` as an attribute to the sub-component. – lex82 Jul 15 '17 at 11:37
28

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

Liam
  • 27,717
  • 28
  • 128
  • 190
Klas Mellbourn
  • 42,571
  • 24
  • 140
  • 158
  • what if the sub component has an input/output property and i want to create a mock for it to put spies on? – baryo Oct 11 '16 at 11:52
  • @baryo in that case I would not recommend the path I have suggested in my answer, since those components will be completely ignored. – Klas Mellbourn Oct 11 '16 at 14:56
  • roger that. do you know a way to achieve that thought? – baryo Oct 12 '16 at 16:56
  • 1
    ok, i found a way: you create a mock directive with the same inputs/outputs & selector as the real subcomponent, and add it to the test module's declaration instead of the real one. now in the test itself you can access its instance like the following: `fixture.debugElement.query(By.directive(MockSubComponent)).injector.get(MockSubComponent)` . Cheers! – baryo Oct 16 '16 at 10:00
  • 1
    @baryo That sounds great! Could you post a plunkr? Or another answer to this question? – Klas Mellbourn Oct 18 '16 at 17:56
9

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');    
  });
});
Liam
  • 27,717
  • 28
  • 128
  • 190
allenhwkim
  • 27,270
  • 18
  • 89
  • 122
6

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.

Christian Nunciato
  • 10,276
  • 2
  • 35
  • 45
  • This will not work if the component have something like this `@ViewChild(SubComponent) sc; sc.callFn()`. Component injector will not recognize `SubComponent` with your mock replacement and will always return null. One solution is to tag it but tagging wont work with `@ContentChild` or dynamic sub components :/ – Julian Mar 17 '17 at 10:35
  • @Nicolas won't this also be a problem with baryo's solution?, now I have done some digging these two solutions look very similar. I only ask because I first read the comment as a hint this solution wasn't as good as the others but Im still learning angular. – rgammans Jun 03 '17 at 19:15
  • @rgammans Yes his solution is nothing more than overriding the tag or selector. When doing something like @ViewChild(SubComponent) it will not work sine the real component will always point to the SubComponent and not let say MockTaskDirective – Julian Jun 05 '17 at 09:35
  • Let's say in the real component 'someOutput' is EventEmitter. Is it possible to emit it inside the unit test using 'ng2-mock-component'? – Pavel Sapehin Oct 02 '17 at 09:47