90

I've got a component that uses the @Input() annotation on an instance variable and I'm trying to write my unit test for the openProductPage() method, but I'm a little lost at how I setup my unit test. I could make that instance variable public, but I don't think I should have to resort to that.

How do I setup my Jasmine test so that a mocked product is injected (provided?) and I can test the openProductPage() method?

My component:

import {Component, Input} from "angular2/core";
import {Router} from "angular2/router";

import {Product} from "../models/Product";

@Component({
    selector: "product-thumbnail",
    templateUrl: "app/components/product-thumbnail/product-thumbnail.html"
})

export class ProductThumbnail {
    @Input() private product: Product;


    constructor(private router: Router) {
    }

    public openProductPage() {
        let id: string = this.product.id;
        this.router.navigate([“ProductPage”, {id: id}]);
    }
}
Kamil Naja
  • 6,267
  • 6
  • 33
  • 47
hartpdx
  • 2,230
  • 2
  • 15
  • 18
  • 2
    I wrote a short blog about testing Components with @Input() that explains a few ways to test the input you want: https://medium.com/@AikoPath/testing-angular-components-with-input-3bd6c07cfaf6 – BraveHeart Jun 22 '17 at 07:08

4 Answers4

74

this is from official documentation https://angular.io/docs/ts/latest/guide/testing.html#!#component-fixture. So you can create new input object expectedHero and pass it to the component comp.hero = expectedHero

Also make sure to call fixture.detectChanges(); last, otherwise property will not be bound to component.

Working Example

// async beforeEach
beforeEach( async(() => {
    TestBed.configureTestingModule({
        declarations: [ DashboardHeroComponent ],
    })
    .compileComponents(); // compile template and css
}));

// synchronous beforeEach
beforeEach(() => {
    fixture = TestBed.createComponent(DashboardHeroComponent);
    comp    = fixture.componentInstance;
    heroEl  = fixture.debugElement.query(By.css('.hero')); // find hero element

    // pretend that it was wired to something that supplied a hero
    expectedHero = new Hero(42, 'Test Name');
    comp.hero = expectedHero;
    fixture.detectChanges(); // trigger initial data binding
});
Evaldas Buinauskas
  • 13,739
  • 11
  • 55
  • 107
Vazgen Manukyan
  • 1,410
  • 1
  • 12
  • 17
  • 8
    where is the hero element used – Aniruddha Das Aug 24 '17 at 15:03
  • Aniruddha Das - it will be used if you bind to any properties of the hero in the html. I had the same problem exactly and this solution is simple to implement, and you get to create a mock object right here in the test. This should be the accepted answer. – Dean Feb 07 '18 at 14:26
  • 3
    Using before each to set data that needs to be dynamic for each test seems like a really bad pattern for writing tests that need to test anything more than one specific case – Captain Prinny Aug 13 '20 at 14:26
  • One important thing to consider, if your class implements `OnInit`: The `ngOnInit()` method is called (only) after the first call of `detectChanges()`. Therefore be carefully with calling `detectChanges()` in `beforeEach`. – Datz Mar 07 '22 at 07:39
56

If you use TestBed.configureTestingModule to compile your test component, here's another approach. It's basically the same as the accepted answer, but may be more similar to how angular-cli generates the specs. FWIW.

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';

describe('ProductThumbnail', () => {
  let component: ProductThumbnail;
  let fixture: ComponentFixture<TestComponentWrapper>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ 
        TestComponentWrapper,
        ProductThumbnail
      ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA]
    })
    .compileComponents();

    fixture = TestBed.createComponent(TestComponentWrapper);
    component = fixture.debugElement.children[0].componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

@Component({
  selector: 'test-component-wrapper',
  template: '<product-thumbnail [product]="product"></product-thumbnail>'
})
class TestComponentWrapper {
  product = new Product()
}
P.M
  • 2,880
  • 3
  • 43
  • 53
Danny Bullis
  • 3,043
  • 2
  • 29
  • 35
  • I am trying what you suggest above.. but when I do, I get a "Uncaught ReferenceError: Zone is not defined" . I am using a virtual clone of the code you have shown above. (with the addition of my own includes as below): `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { testContentNavData } from './mok-definitions'; import { ContentNavComponent } from '../app/content-nav/content-nav.component'; import {} from 'jasmine';` – Kim Gentes Jan 11 '17 at 23:40
  • That looks like a Zone.js error, so it's hard to say. Are you using Angular CLI? Perhaps provide a link to the full error getting logged in your console. – Danny Bullis Feb 08 '17 at 19:30
  • I followed your approached however my component being tested having the template '

    ' and it never passes tests. Everything works fine, component gets rendered correctly but html is not added. If i change to

    {{ customFieldFormatted }}

    everything works fine. Not sure why [outerHTML] does not work. Do you have any idea? thank you
    – Alex Ryltsov Sep 19 '17 at 16:35
  • @KimGentes, I believe, some provider configuration is missing which resulted in 'Uncaught ReferenceError: Zone is not defined' issue. What I do in such scenario is adding try-catch block around `TestBed.configureTestingModule()` and write the error to console. That shows which provider is missing. Just adding this comment so that in future it may help someone. – ramtech Jan 18 '18 at 09:33
  • I think this answer needs to be improved, it doesn't go all the way to demonstrate how one is to not use a static Product on the wrapper component, thus leading a naive person to write a component wrapper for every test case of distinct product as input. – Captain Prinny Aug 13 '20 at 14:23
  • @CaptainPrinny what would you suggest? If I understand what you're saying correctly, you're suggesting that the developer would like to provide different instances of `Product` with different property values specified for different test cases, is that correct? I can't immediately see how to do that. If you do, feel free to share and I'll update the answer, because I see the value in what you're saying. – Danny Bullis Nov 02 '21 at 01:43
32

You need to set the product value on the component instance after it has been loaded within your test.

As a sample here is a simple component within an input that you can use as a foundation for your use case:

@Component({
  selector: 'dropdown',
  directives: [NgClass],
  template: `
    <div [ngClass]="{open: open}">
    </div>
  `,
})
export class DropdownComponent {
  @Input('open') open: boolean = false;

  ngOnChanges() {
    console.log(this.open);
  }
}

And the corresponding test:

it('should open', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
  return tcb.createAsync(DropdownComponent)
  .then(fixture => {
    let el = fixture.nativeElement;
    let comp: DropdownComponent = fixture.componentInstance;

    expect(el.className).toEqual('');

    // Update the input
    comp.open = true; // <-----------

    // Apply
    fixture.detectChanges(); // <-----------

    var div = fixture.nativeElement.querySelector('div');
    // Test elements that depend on the input
    expect(div.className).toEqual('open');
  });
}));

See this plunkr as a sample: https://plnkr.co/edit/YAVD4s?p=preview.

Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • 3
    In OP's example, the `@Input` property being set is private. Unless I'm mistaken, this approach is not going to work in that case, because tsc is going to barf on the reference to a private field. – drew moore Apr 15 '16 at 19:43
  • 2
    Thanks for pointing this out! I missed that the field was private. I thought again about your comment and the "private" aspect. I wonder if it's a good thing to have the `private` keyword on this field since it's not actually "private"... I mean it will be updated from outside the class by Angular2. Would be interested in having your opinion ;-) – Thierry Templier Apr 17 '16 at 12:01
  • 2
    you ask an interesting question, but I think the real question you have to ask then is whether it's a good thing to have `private` in typescript at all since it's not "actually private" - i.e., since it can't be enforced at runtime, only at compile time. I personally like it, but also understand the argument against it. At the end of the day though, Microsoft choose to have it in TS, and Angular chose TS as a principal language, and I don't think we can flatly say it's a bad idea to use a major feature of a primary language. – drew moore Apr 18 '16 at 01:09
  • 3
    Thanks very much for your answer! I'm personally convinced that using TypeScript is a good thing. It actually contributes to improve application quality! I don't think that using `private` is a bad thing even if it's not really private at runtime :-) That said for this particular case, I'm not sure that is a good thing to use `private` since the field is managed outside the class by Angular2... – Thierry Templier Apr 20 '16 at 07:29
  • 3
    I'm trying to use it with the new TestBed.createComponent but when I call fixture.detectChanges() it does not trigger ngOnChanges call. Do you know how can I test it with the "new system"? – bucicimaci Oct 07 '16 at 08:25
  • the `TestComponentBuilder` class has been replaced by the [TestBed](https://angular.io/api/core/testing/TestBed) – Cichy Mar 03 '21 at 08:23
18

I usually do something like:

describe('ProductThumbnail', ()=> {
  it('should work',
    injectAsync([ TestComponentBuilder ], (tcb: TestComponentBuilder) => {
      return tcb.createAsync(TestCmpWrapper).then(rootCmp => {
        let cmpInstance: ProductThumbnail =  
               <ProductThumbnail>rootCmp.debugElement.children[ 0 ].componentInstance;

        expect(cmpInstance.openProductPage()).toBe(/* whatever */)
      });
  }));
}

@Component({
 selector  : 'test-cmp',
 template  : '<product-thumbnail [product]="mockProduct"></product-thumbnail>',
 directives: [ ProductThumbnail ]
})
class TestCmpWrapper { 
    mockProduct = new Product(); //mock your input 
}

Note that product and any other fields on the ProductThumbnail class can be private with this approach (which is the main reason I prefer it over Thierry's approach, despite the fact that it's a little more verbose).

drew moore
  • 31,565
  • 17
  • 75
  • 112
  • Do you still need to inject TestComponentBuilder? see: https://medium.com/@AikoPath/testing-angular-components-with-input-3bd6c07cfaf6 – BraveHeart Jun 22 '17 at 07:09
  • For developers who seek the "pure testbed" approach there are some answers down there in this post: https://stackoverflow.com/a/36655501/301603 and https://stackoverflow.com/a/43755910/301603 This particular answer is not wrong, but it is more of a 'hack' than real unit test approach – Edgar Zagórski Nov 24 '17 at 17:32