在Angular 10+中测试具有ViewChildren的组件时,如何使用伪造/模拟/存根子组件? [英] How can I use a fake/mock/stub child component when testing a component that has ViewChildren in Angular 10+?

查看:28
本文介绍了在Angular 10+中测试具有ViewChildren的组件时,如何使用伪造/模拟/存根子组件?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在将其标记为

Before marking this as a duplicate of this question please note that I'm asking specifically about Angular 10+, because the answers to that question no longer work as of Angular 10.

我创建了一个简单的示例应用程序,可以帮助说明我的问题.这个应用程式的想法是几个人"会说你好",您可以通过键入他们的名字来回复他们中的任何一个.看起来像这样:

I've created a simple example app that helps illustrates my question. The idea with this app is that several "people" will say "hello", and you can respond to any or all of them by typing their name. It looks like this:

(请注意,来自Sue的"hello"已变灰,因为我在文本框中输入了"sue"作为答复).

(Note that the 'hello' from Sue has been greyed out because I responded by typing "sue" in the text box).

您可以在 StackBlitz 中使用此应用程序.

You can play with this app in a StackBlitz.

如果查看该应用程序的代码,则会看到有两个组件:AppComponentHelloComponent. AppComponent为每个人"呈现一个HelloComponent.

If you look at the code for the app, you'll see that there are two components: AppComponent and HelloComponent. The AppComponent renders one HelloComponent for each "person".

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类具有一个称为'hellos'的ViewChildren属性.此属性在answer方法中使用,并在相应的HelloComponent上调用answer方法:

The AppComponent class has a ViewChildren property called 'hellos'. This property is used in the answer method, and calls the answer method on the appropriate HelloComponent:

app.component.ts

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html'
})
  export class AppComponent  {
  public names = ['Bob', 'Sue', 'Rita'];

  @ViewChildren(HelloComponent) public hellos: QueryList<HelloComponent>;

  public answer(name: string): void {
    const hello = this.hellos.find(h => h.name.toUpperCase() === name.toUpperCase());
    if (hello) {
      hello.answer();
    }
  }
}

到目前为止,一切都很好-并且一切正常.但是现在我要对AppComponent ...

So far, so good - and that all works. But now I want to unit-test the AppComponent...

因为我正在测试单元,所以我不希望我的测试依赖于HelloComponent的实现(而且我肯定是 (不想依赖它可能使用的任何服务等),因此我将通过创建存根组件来模拟HelloComponent:

Because I'm unit testing the AppComponent, I don't want my test to depend on the implementation of the HelloComponent (and I definitely don't want to depend on any services etc. that it might use), so I'll mock out the HelloComponent by creating a stub component:

@Component({
  selector: "hello",
  template: "",
  providers: [{ provide: HelloComponent, useClass: HelloStubComponent }]
})
class HelloStubComponent {
  @Input() public name: string;
  public answer = jasmine.createSpy("answer");
}

在适当的位置,我的单元测试可以创建AppComponent并验证三个"hello"是否正确.项目已创建:

With that in place, my unit tests can create the AppComponent and verify that three "hello" items are created:

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

...这很好.但是,如果我尝试测试组件的answer()方法的实际行为(以检查它是否调用了正确的HelloComponentanswer()方法,那么它将失败:

...which is good. But, if I try to test the actual behaviour of the component's answer() method (to check that it calls the answer() method of the correct HelloComponent, then it fails:

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

执行此测试时,会发生错误:

When this test executes, an error occurs:

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

TypeError: Cannot read property 'toUpperCase' of undefined

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

This error occurs in the answer() method of AppComponent:

public answer(name: string): void {
  const hello = this.hellos.find(h => h.name.toUpperCase() === name.toUpperCase());
  if (hello) {
    hello.answer();
  }
}

发生的是lambda中的h.nameundefined.为什么?

What's happening is that h.name in the lambda is undefined. Why?

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

I can illustrate the problem more succinctly with another unit test:

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

此操作失败:

错误:预期未定义.

Error: Expected undefined to be defined.

错误:预期未定义.

错误:预期未定义.

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

Although the results are of type HelloStubComponent, the name property is not set.

我认为这是因为ViewChildren属性期望实例的类型为HelloComponent而不是HelloStubComponent(这很公平,因为它是这样声明的)-某种程度上这使事情变得混乱了.

I assume that this is because the ViewChildren property is expecting the instances to be of type HelloComponent and not HelloStubComponent (which is fair, because that's how it's declared) - and somehow this is messing things up.

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

You can see the unit tests in action in this alternative StackBlitz. (It has the same components but is set up to launch Jasmine instead of the app; to switch between "test" mode and "run" mode, edit angular.json and change "main": "src/test.ts" to "main": "src/main.ts" and restart).

因此:如何在组件内获取QueryList以便与我的存根组件正常工作?我看到了一些建议:

So: how can I get the QueryList within the component to work properly with my stub components? I've seen several suggestions:

  1. 如果该属性是使用ViewChild而不是ViewChildren的单个组件,则只需在测试中覆盖该属性的值即可.这相当丑陋,无论如何对ViewChildren都无济于事.

  1. Where the property is a single component using ViewChild rather than ViewChildren, simply overwrite the value of the property in the test. This is rather ugly, and in any case it doesn't help with ViewChildren.

问题有一个涉及propMetadata的答案,该答案有效地更改了Angular期望QueryList中的项目的类型.可接受的答案一直持续到Angular 5为止,还有另一个答案适用于Angular 5(实际上,我能够将其用于Angular 9).但是,此不再适用于Angular 10 -大概是因为v10再次更改了它所依赖的内部未记录内部结构.

This question has an answer involving propMetadata that effectively changes what type Angular expects the items in the QueryList to be. The accepted answer worked up until Angular 5, and there's another answer that worked with Angular 5 (and in fact I was able to use that for Angular 9). However, this no longer works in Angular 10 - presumably because the undocumented internals that it relies on have changed again with v10.

所以,我的问题是:还有另一种方法可以实现这一目标吗?还是有办法再次在Angular 10+中破解propMetadata?

So, my question is: is there another way to achieve this? Or is there a way to once again hack the propMetadata in Angular 10+?

推荐答案

当您需要模拟子组件时,请考虑使用 ng-mocks .它支持包括ViewChildren在内的所有Angular功能.

When you need a mock child component, consider usage of ng-mocks. It supports all Angular features including ViewChildren.

然后HelloComponent组件将被其模拟对象替换,并且不会在测试中引起任何副作用.最好的是,不需要创建stub组件.

Then HelloComponent component will be replaced with its mock object and won't cause any side effects in the test. The best thing here is that there is no need in creating stub components.

有一个有效的示例: https://codesandbox.io/s/wizardly-shape-8wi3i?file=/src/test.spec.ts&initialpath=%3Fspec%3DAppComponent

beforeEach(() => TestBed.configureTestingModule({
  declarations: [AppComponent, MockComponent(HelloComponent)],
}).compileComponents());

// better, because if HelloComponent has been removed from
// AppModule, the test will fail.
// beforeEach(() => MockBuilder(AppComponent, AppModule));

// Here we inject a spy into HelloComponent.answer 
beforeEach(() => MockInstance(HelloComponent, 'answer', jasmine.createSpy()));

// Usually MockRender should be called right in the test.
// It returns a fixture
beforeEach(() => MockRender(AppComponent));

it("should have 3 hello components", () => {
  // ngMocks.findAll is a short form for queries.
  const hellos = ngMocks.findAll(HelloComponent);
  expect(hellos.length).toBe(3);
});

it("should be able to access the 3 hello components as ViewChildren", () => {
  // the AppComponent
  const component = ngMocks.findInstance(AppComponent);

  // All its properties have been defined correctly
  expect(component.hellos).toBeDefined();
  expect(component.hellos.length).toBe(3);

  // ViewChildren works properly
  component.hellos.forEach(h => {
    expect(h).toEqual(jasmine.any(HelloComponent));
    expect(h.name).toBeDefined(); // WORKS
  });
});

it("should answer Bob", () => {
  const component = ngMocks.findInstance(AppComponent);
  const hellos = ngMocks.findAll(HelloComponent);
  const bob = hellos.find(h => h.componentInstance.name === "Bob");
  
  expect(bob.componentInstance.answer).not.toHaveBeenCalled();
  component.answer("Bob"); // WORKS
  expect(bob.componentInstance.answer).toHaveBeenCalled();
  });

这篇关于在Angular 10+中测试具有ViewChildren的组件时,如何使用伪造/模拟/存根子组件?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆