97

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}]);
    }
}
1
  • 3
    I wrote a short blog about testing Components with @Input() that explains a few ways to test the input you want: medium.com/@AikoPath/… Commented Jun 22, 2017 at 7:08

4 Answers 4

79

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
});
Sign up to request clarification or add additional context in comments.

4 Comments

where is the hero element used
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.
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
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.
57

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()
}

6 Comments

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';
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.
I followed your approached however my component being tested having the template '<p [outerHTML]="customFieldFormatted"></p>' and it never passes tests. Everything works fine, component gets rendered correctly but html is not added. If i change to <p>{{ customFieldFormatted }}</p> everything works fine. Not sure why [outerHTML] does not work. Do you have any idea? thank you
@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.
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.
|
34

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.

6 Comments

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.
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 ;-)
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.
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...
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"?
|
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).

2 Comments

Do you still need to inject TestComponentBuilder? see: medium.com/@AikoPath/…
For developers who seek the "pure testbed" approach there are some answers down there in this post: stackoverflow.com/a/36655501/301603 and stackoverflow.com/a/43755910/301603 This particular answer is not wrong, but it is more of a 'hack' than real unit test approach

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.