【问题标题】:angular2 test, how do I mock sub componentangular2 测试,我如何模拟子组件
【发布时间】:2016-06-28 19:16:31
【问题描述】:

如何在 jasmine 测试中模拟子组件?

我有MyComponent,它使用MyNavbarComponentMyToolbarComponent

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 {}

当我测试这个组件时,我不想加载和测试这两个子组件; MyNavbarComponent,MyToolbarComponent,所以我想模拟一下。

我知道如何使用provide(MyService, useClass(...)) 模拟服务,但我不知道如何模拟指令;组件;

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

这是我的 plunker 示例;

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

【问题讨论】:

  • 组件工作正常,你的问题是另一回事。例如,您正在导入MyNavbarComponent,但在您的组件类中称为myNavbarComponent。注意小写的m,它会导致失败。如果你大写它会正常工作。
  • 感谢@EricMartinez,我修复了小写字母并且测试有效。但是,我的问题对于如何模拟组件仍然有效。我正在测试MyComponent,不是MyNavbarComponent,也不是MyToolbarComponent
  • 是的,很抱歉。你可以看看这个spec,看看他们是如何模拟组件的。
  • @EricMartinez,谢谢。我发布了从您的 commnet 中学到的自己的答案。所有功劳归您所有。

标签: unit-testing testing jasmine angular components


【解决方案1】:

根据要求,我将发布另一个关于如何使用input/output 模拟子组件的答案:

首先让我们说我们有TaskListComponent,它显示任务,并在单击其中一个时刷新:

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

app-task 是具有[task] 输入和(click) 输出的子组件。

好的,现在我们想为我的TaskListComponent 编写测试,当然我们不想测试真正的app-task组件。

所以正如@Klas 建议的那样,我们可以配置我们的TestModule

schemas: [CUSTOM_ELEMENTS_SCHEMA]

我们可能在构建或运行时都不会遇到任何错误,但除了子组件的存在之外,我们将无法进行太多测试。

那么我们如何模拟子组件呢?

首先,我们将为子组件(相同的选择器)定义一个模拟指令:

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

现在我们将在测试模块中声明它:

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;
});
  • 请注意,由于夹具子组件的生成是在创建后异步发生的,因此我们激活了它的 autoDetectChanges 功能。

在我们的测试中,我们现在可以查询指令,访问其 DebugElement 的注入器,并通过它获取我们的模拟指令实例:

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

[这部分通常应该在beforeEach 部分,以使代码更简洁。]

从这里开始,测试是小菜一碟:)

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

【讨论】:

  • 您可能想要一个调用fixture.detectChanges() 的示例。我的应用程序的子组件的数据仅在方法调用中设置,所以为了让这个示例对我有用,我必须在夹具上调用该方法,然后调用 detectChanges() 使其工作。
  • By.directive 是不是需要单独导入才能使用? - 我的 Karma 测试出现错误:“ReferenceError: By is not defined”
  • @Alex 我也在想同样的事情。import { By } from '@angular/platform-browser'; 为我工作。
  • 多么棒的答案。节省了我很多时间!
  • 然而,当父母使用@ViewChild(SubComponent) 时,此解决方案无法解决问题。仍在等待 Angular 团队解决此问题
【解决方案2】:

如果你在TestBed中使用schemas: [CUSTOM_ELEMENTS_SCHEMA],被测组件将不会加载子组件。

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

});

这适用于已发布的 Angular 2.0 版本。 Full code sample here.

CUSTOM_ELEMENTS_SCHEMA 的替代品是NO_ERRORS_SCHEMA

【讨论】:

  • 如果子组件有输入/输出属性,我想为它创建一个模拟来放置间谍怎么办?
  • @baryo 在这种情况下,我不会推荐我在回答中建议的路径,因为这些组件将被完全忽略。
  • 罗杰。你知道实现这个想法的方法吗?
  • 好的,我找到了一种方法:创建一个具有与真实子组件相同的输入/输出和选择器的模拟指令,并将其添加到测试模块的声明中,而不是真实的。现在在测试本身中,您可以访问它的实例,如下所示:fixture.debugElement.query(By.directive(MockSubComponent)).injector.get(MockSubComponent)。干杯!
  • @baryo 听起来不错!你能发布一个plunkr吗?或者这个问题的另一个答案?
【解决方案3】:

感谢 Eric Martinez,我找到了这个解决方案。

我们可以使用此处记录的overrideDirective 函数, https://angular.io/docs/ts/latest/api/testing/TestComponentBuilder-class.html

需要三个参数; 1. 要实现的组件 2.要覆盖的子组件 3. 模拟组件

已解决的解决方案在http://plnkr.co/edit/a71wxC?p=preview

这是来自 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');    
  });
});

【讨论】:

  • 很高兴知道它有帮助!
  • karma-babel 是否正确处理@Component 注释?你能发布你的 karma.conf 吗?我在那条线上得到[preprocessor.babel]: Unexpected token
  • 如何在 Angular ^2.0.0 中使用 TestBed 完成此操作?
  • @PhilipBulley 看到我的回答
  • TestComponentBuilder 已被弃用,新的​​TestBed. overrideDirective 只允许覆盖元数据
【解决方案4】:

我整理了一个简单的MockComponent 模块来帮助简化此操作:

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

  ...
});

它可以在 https://www.npmjs.com/package/ng2-mock-component.

【讨论】:

  • 如果组件有类似@ViewChild(SubComponent) sc; sc.callFn() 的东西,这将不起作用。组件注入器将无法通过您的模拟替换识别 SubComponent,并且将始终返回 null。一种解决方案是标记它,但标记不适用于@ContentChild 或动态子组件:/
  • @Nicolas 这不是 baryo 解决方案的问题吗?现在我已经做了一些挖掘,这两个解决方案看起来非常相似。我之所以问,是因为我第一次阅读评论是为了暗示这个解决方案不如其他解决方案好,但我仍在学习角度。
  • @rgammans 是的,他的解决方案只不过是覆盖标签或选择器。当做类似 @ViewChild(SubComponent) 的事情时,它不会工作,因为真正的组件总是指向 SubComponent 而不是说 MockTaskDirective
  • 假设在真正的组件中'someOutput'是EventEmitter。是否可以使用“ng2-mock-component”在单元测试中发出它?
猜你喜欢
  • 1970-01-01
  • 2017-04-19
  • 2017-03-14
  • 1970-01-01
  • 1970-01-01
  • 2016-04-18
  • 1970-01-01
  • 2017-05-10
  • 2020-11-30
相关资源
最近更新 更多