Angular2中使用@Input()进行单元测试

94

我有一个组件,该组件在实例变量上使用了 @Input() 注释,并且我正在尝试编写针对 openProductPage() 方法的单元测试,但是我不太清楚如何设置我的单元测试。我可以将该实例变量设置为公共的,但我认为我不应该采取这种方法。

我该如何设置我的Jasmine测试以便注入(提供)模拟产品并测试 openProductPage() 方法?

我的组件:

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}]);
    }
}

2
我写了一篇关于使用@Input()测试组件的简短博客,解释了几种测试所需输入的方法:https://medium.com/@AikoPath/testing-angular-components-with-input-3bd6c07cfaf6 - BraveHeart
4个回答

76

这是来自官方文档https://angular.io/docs/ts/latest/guide/testing.html#!#component-fixture的内容。因此,您可以创建新的输入对象 expectedHero 并将其传递给组件 comp.hero = expectedHero

还要确保最后调用fixture.detectChanges();,否则属性将无法绑定到组件。

工作示例

// 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
});

8
英雄元素在哪里使用? - Aniruddha Das
Aniruddha Das - 如果您在HTML中绑定到任何英雄属性,则将使用它。我完全遇到了同样的问题,这个解决方案很容易实现,而且您可以在测试中创建一个模拟对象。这应该是被接受的答案。 - Dean
3
在撰写需要测试不止一个特定情况的测试时,使用每个测试之前设置需要动态更改的数据似乎是一种非常糟糕的模式。 - Captain Prinny
有一件重要的事情需要考虑,如果你的类实现了 OnInit 接口:ngOnInit() 方法只会在第一次调用 detectChanges() 后被调用。因此,在 beforeEach 中调用 detectChanges() 时要小心。 - Datz

56

如果您使用TestBed.configureTestingModule编译测试组件,这是另一种方法。基本上与被接受的回答相同,但可能更类似于angular-cli生成规范的方式。 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()
}

我正在尝试您上面建议的方法...但是当我这样做时,我会得到一个"Uncaught ReferenceError: Zone is not defined"的错误。我正在使用您上面展示的代码的虚拟克隆。(加上我的自己的包含如下): 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
那看起来像是一个Zone.js错误,所以很难说。你正在使用Angular CLI吗?也许提供一个链接到完整的错误日志记录在你的控制台中。 - Danny Bullis
我按照你的方法进行了操作,但是我的被测试组件有一个模板 '<p [outerHTML]="customFieldFormatted"></p>',它从未通过测试。一切都正常,组件正确地呈现,但是HTML没有添加。如果我改为 <p>{{ customFieldFormatted }}</p>,一切都正常。不确定为什么 [outerHTML] 不起作用。你有任何想法吗?谢谢 - Alex Ryltsov
@KimGentes,我认为缺少某些提供程序配置导致了“未捕获的ReferenceError:Zone未定义”的问题。在这种情况下,我会在TestBed.configureTestingModule()周围添加try-catch块,并将错误写入控制台。这样可以显示哪个提供程序丢失。 只是添加此评论以便将来有所帮助。 - ramtech
我认为这个答案需要改进,它没有完全展示如何在包装组件上不使用静态产品,因此会导致一个天真的人为每个测试用例编写一个组件包装器。 - Captain Prinny
@CaptainPrinny 你有什么建议吗?如果我正确理解了您的意思,您建议开发人员希望针对不同的测试用例提供具有不同属性值的 Product 实例,是这样吗? 我不能立即看到如何做到这一点。如果您知道,请随时分享,我会更新答案,因为我认为您的建议很有价值。 - Danny Bullis

32

在你的测试中加载组件实例后,需要设置product值。

这里有一个简单的组件示例,包含一个输入框,你可以将其作为你的用例基础:

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

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

以及相应的测试:

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');
  });
}));

查看此plunkr作为示例:https://plnkr.co/edit/YAVD4s?p=preview


3
根据OP的示例,被设置的@Input属性是私有的。除非我弄错了,否则在这种情况下,这种方法不会起作用,因为tsc将在引用私有字段时报错。 - drew moore
2
谢谢指出这个问题!我错过了这个字段是私有的。我再次考虑了你的评论和“私有”方面。我想知道在这个字段上使用private关键字是否是一个好主意,因为它实际上并不是“私有的”...我的意思是,它将被Angular2从类外部更新。很想听听你的意见;-) - Thierry Templier
2
你提出了一个有趣的问题,但我认为你真正需要问的问题是,在 TypeScript 中是否应该拥有 private,因为它并不是“真正的私有”——也就是说,它不能在运行时强制执行,只能在编译时执行。我个人喜欢它,但也理解反对它的观点。不过,最终微软选择在 TS 中使用它,Angular 选择 TS 作为主要语言,我们不能断言使用主要语言的重要特性是一个坏主意。 - drew moore
3
非常感谢您的回答!我个人认为使用TypeScript是一件好事。它实际上有助于提高应用程序的质量!我认为即使在运行时private不完全私有,使用private也不是一件坏事 :-) 话虽如此,在这种特定情况下,我不确定使用private是否是一个好主意,因为该字段由Angular2在类外部进行管理... - Thierry Templier
3
我想使用新的TestBed.createComponent方法进行测试,但是当我调用fixture.detectChanges()时,它不会触发ngOnChanges调用。你知道如何用“新系统”进行测试吗? - bucicimaci
TestComponentBuilder 类已被 TestBed 取代。 - Cichy

18

我通常会做类似这样的事情:

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 
}

请注意,使用这种方法,ProductThumbnail类中的product和其他任何字段可以是私有的(这也是我喜欢它胜过Thierry's方法的主要原因,尽管它可能会显得有点啰嗦)。


你还需要注入TestComponentBuilder吗?请参考:https://medium.com/@AikoPath/testing-angular-components-with-input-3bd6c07cfaf6 - BraveHeart
对于寻求“纯测试平台”方法的开发人员,本文中有一些答案:https://dev59.com/eVoV5IYBdhLWcg3wIb34#36655501 和 https://dev59.com/eVoV5IYBdhLWcg3wIb34#43755910这个特定的答案并没有错,但它更像是一个“hack”,而不是真正的单元测试方法。 - Edgar Zagórski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接