Warning: file_get_contents(/data/phpspider/zhask/data//catemap/1/angular/27.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
当测试在Angular 10+;中具有ViewChildren的组件时,如何使用假/模拟/存根子组件;?_Angular_Unit Testing_Jasmine - Fatal编程技术网

当测试在Angular 10+;中具有ViewChildren的组件时,如何使用假/模拟/存根子组件;?

当测试在Angular 10+;中具有ViewChildren的组件时,如何使用假/模拟/存根子组件;?,angular,unit-testing,jasmine,Angular,Unit Testing,Jasmine,在将此标记为的副本之前,请注意,我特别询问的是Angular 10+,因为该问题的答案在Angular 10之后不再有效 背景 我创建了一个简单的示例应用程序,可以帮助说明我的问题。这个应用程序的想法是,几个“人”会说“你好”,你可以通过键入他们的名字来回复他们中的任何人或所有人。看起来是这样的: (请注意,Sue的“hello”已变灰,因为我在文本框中键入“Sue”作为回应) 您可以在游戏中使用此应用程序 如果查看应用程序的代码,您将看到有两个组件:AppComponent和HelloCo

在将此标记为的副本之前,请注意,我特别询问的是Angular 10+,因为该问题的答案在Angular 10之后不再有效


背景 我创建了一个简单的示例应用程序,可以帮助说明我的问题。这个应用程序的想法是,几个“人”会说“你好”,你可以通过键入他们的名字来回复他们中的任何人或所有人。看起来是这样的:

(请注意,Sue的“hello”已变灰,因为我在文本框中键入“Sue”作为回应)

您可以在游戏中使用此应用程序

如果查看应用程序的代码,您将看到有两个组件:
AppComponent
HelloComponent
AppComponent
为每个“人”呈现一个
HelloComponent

app.component.html

<ng-container *ngFor="let n of names">
  <hello name="{{n}}"></hello>
</ng-container>
<hr/>
<h2>Type the name of whoever you want to respond to:</h2>
Hi <input type='text' #text (input)="answer(text.value)" />
到目前为止,一切正常。但是现在我想对AppComponent进行单元测试

添加单元测试 因为我正在对
AppComponent
进行单元测试,所以我不希望我的测试依赖于
HelloComponent
的实现(而且我绝对不希望依赖于它可能使用的任何服务等),所以我将通过创建存根组件模拟
HelloComponent

@Component({
  selector: "hello",
  template: "",
  providers: [{ provide: HelloComponent, useClass: HelloStubComponent }]
})
class HelloStubComponent {
  @Input() public name: string;
  public answer = jasmine.createSpy("answer");
}
这样,我的单元测试就可以创建
AppComponent
,并验证是否创建了三个“hello”项:

it("should have 3 hello components", () => {
  // If we make our own query then we can see that the ngFor has produced 3 items
  const hellos = fixture.debugElement.queryAll(By.css("hello"));
  expect(hellos).not.toBeNull();
  expect(hellos.length).toBe(3);
});
const hellos = fixture.debugElement.queryAll(By.css('hello'));
const components = hellos.map(h => h.componentInstance);
fixture.componentInstance.hellos.reset(components);
…这很好。但是,如果我尝试测试组件的
answer()
方法的实际行为(检查它是否调用了正确的
HelloComponent
answer()
方法),那么它将失败:

it("should answer Bob", () => {
  const hellos = fixture.debugElement.queryAll(By.css("hello"));
  const bob = hellos.find(h => h.componentInstance.name === "Bob");
  // bob.componentInstance is a HelloStubComponent

  expect(bob.componentInstance.answer).not.toHaveBeenCalled();
  fixture.componentInstance.answer("Bob");
  expect(bob.componentInstance.answer).toHaveBeenCalled();
});
执行此测试时,会发生错误:

TypeError:无法读取未定义的属性“toUpperCase”

此错误发生在
AppComponent
answer()
方法中:

public answer(name: string): void {
  const hello = this.hellos.find(h => h.name.toUpperCase() === name.toUpperCase());
  if (hello) {
    hello.answer();
  }
}
发生的事情是lambda中的
h.name
未定义的
。为什么

我可以用另一个单元测试更简洁地说明这个问题:

it("should be able to access the 3 hello components as ViewChildren", () => {
  expect(fixture.componentInstance.hellos).toBeDefined();
  expect(fixture.componentInstance.hellos.length).toBe(3);

  fixture.componentInstance.hellos.forEach(h => {
    expect(h).toBeDefined();
    expect(h.constructor.name).toBe("HelloStubComponent");
    // ...BUT the name property is not set
    expect(h.name).toBeDefined(); // FAILS
  });
});
这失败了:

错误:应为未定义的

错误:应为未定义的

错误:应为未定义的

尽管结果的类型为
HelloSubComponent
,但未设置
name
属性

我假设这是因为
ViewChildren
属性期望实例的类型为
HelloComponent
,而不是
hellossubcomponent
(这是公平的,因为它是这样声明的)-不知怎的,这会把事情搞砸

您可以在这个备选方案中查看正在运行的单元测试(它具有相同的组件,但设置为启动Jasmine而不是应用程序;要在“测试”模式和“运行”模式之间切换,请编辑
angular.json
,并将
“main”:“src/test.ts”
更改为
“main”:“src/main.ts”
,然后重新启动)

问题: 那么:如何让组件中的
QueryList
与我的存根组件正常工作呢?我看到了一些建议:

  • 如果属性是使用
    ViewChildren
    而不是
    ViewChildren
    的单个组件,只需在测试中覆盖属性的值。这相当难看,而且在任何情况下,它对
    ViewChildren
    都没有帮助

  • 这有一个涉及
    propMetadata
    的答案,它有效地改变了Angular对
    QueryList
    中项目的期望类型。接受的答案一直持续到Angular 5,还有另一个答案对Angular 5有效(事实上,我能够将其用于Angular 9)但是,这个不再适用于Angular 10——可能是因为它所依赖的未记录的内部结构在v10中再次发生了变化


  • 所以,我的问题是:有没有其他方法来实现这一点?或者有没有方法再次破解Angular 10+中的
    propMetadata

    我能够让一些东西“工作”,但我不喜欢它

    由于
    QueryList
    类有一个
    reset()
    方法允许我们更改结果,因此我可以在测试开始时执行此操作,以将结果更改为指向创建的存根组件:

    it("should have 3 hello components", () => {
      // If we make our own query then we can see that the ngFor has produced 3 items
      const hellos = fixture.debugElement.queryAll(By.css("hello"));
      expect(hellos).not.toBeNull();
      expect(hellos.length).toBe(3);
    });
    
    const hellos = fixture.debugElement.queryAll(By.css('hello'));
    const components = hellos.map(h => h.componentInstance);
    fixture.componentInstance.hellos.reset(components);
    
    这“修复”了测试,但我不确定它有多脆弱。可能任何后续执行
    detectChanges
    的操作都将重新计算
    QueryList
    的结果,我们将回到原点


    这里是我把代码放在<代码>之前的方法,以便它适用于所有的测试(现在通过)。

    < P>当您需要一个模拟子组件时,考虑使用它。它支持所有的角特征,包括代码> VIEWSBODS < < /P> 然后,
    HelloComponent
    组件将被其模拟对象替换,并且不会在测试中产生任何副作用。这里最好的一点是,不需要创建
    stub
    组件

    有一个有效的例子:

    beforeach(()=>TestBed.configureTestingModule({
    声明:[AppComponent,MockComponent(HelloComponent)],
    }).compileComponents());
    //更好,因为如果HelloComponent已从
    //AppModule,测试将失败。
    //之前(()=>MockBuilder(AppComponent,AppModule));
    //在这里,我们向HelloComponent中注入一个间谍
    beforeach(()=>MockInstance(HelloComponent,'answer',jasmine.createSpy());
    //通常,MockRender应该在测试中正确调用。
    //它返回一个fixture
    在每个(()=>MockRender(AppComponent))之前;
    它(“应该有3个hello组件”,()=>{
    //ngMocks.findAll是查询的缩写形式。
    常量hellos=ngMocks.findAll(HelloComponent);
    期望(他的长度)。托比(3);
    });
    它(“应该能够作为ViewChildren访问3个hello组件”,()=>{
    //AppComponent
    常数分量=