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