简体   繁体   English

"Angular 2:如何在单元测试时模拟 ChangeDetectorRef"

[英]Angular 2: How to mock ChangeDetectorRef while unit testing

I have just started with Unit-Testing, and I have been able to mock my own services and some of Angular and Ionic as well, but no matter what I do ChangeDetectorRef<\/code> stays the same.我刚刚开始进行单元测试,我已经能够模拟我自己的服务以及一些 Angular 和 Ionic,但无论我做什么, ChangeDetectorRef<\/code>都保持不变。

I mean which kind of sorcery is this?我的意思是这是什么魔法?

beforeEach(async(() => 
    TestBed.configureTestingModule({
      declarations: [MyComponent],
      providers: [
        Form, DomController, ToastController, AlertController,
        PopoverController,

        {provide: Platform, useClass: PlatformMock},
        {
          provide: NavParams,
          useValue: new NavParams({data: new PageData().Data})
        },
        {provide: ChangeDetectorRef, useClass: ChangeDetectorRefMock}

      ],
      imports: [
        FormsModule,
        ReactiveFormsModule,
        IonicModule
      ],
    })
    .overrideComponent(MyComponent, {
      set: {
        providers: [
          {provide: ChangeDetectorRef, useClass: ChangeDetectorRefMock},
        ],
        viewProviders: [
          {provide: ChangeDetectorRef, useClass: ChangeDetectorRefMock},
        ]
      }
    })
    .compileComponents()
    .then(() => {
      let fixture = TestBed.createComponent(MyComponent);
      let cmp = fixture.debugElement.componentInstance;

      let cdRef = fixture.debugElement.injector.get(ChangeDetectorRef);

      console.log(cdRef); // logs ChangeDetectorRefMock
      console.log(cmp.cdRef); // logs ChangeDetectorRef , why ??
    })
  ));

Update 2020 : 2020 年更新

I wrote this originally in May 2017, it's a solution that worked great at the time and still works.我最初是在 2017 年 5 月写的,这是一个在当时非常有效并且仍然有效的解决方案。

We can't configure the injection of a changeDetectorRef mock through the test bed, so this is what I am doing these days:我们无法通过测试台配置 changeDetectorRef 模拟的注入,所以这就是我最近在做的事情:

 it('detects changes', () => {
      // This is a unique instance here, brand new
      const changeDetectorRef = fixture.debugElement.injector.get(ChangeDetectorRef); 
     
      // So, I am spying directly on the prototype.
      const detectChangesSpy = spyOn(changeDetectorRef.constructor.prototype, 'detectChanges');

      component.someMethod(); // Which internally calls the detectChanges.

      expect(detectChangesSpy).toHaveBeenCalled();
    });

Then you don't care about private attributes or any.那么你不关心私有属性或任何。


In case anyone runs into this, this is one way that has worked well for me:万一有人遇到这种情况,这是一种对我很有效的方法:

As you are injecting the ChangeDetectorRef instance in your constructor:当您在构造函数中注入 ChangeDetectorRef 实例时:

 constructor(private cdRef: ChangeDetectorRef) { }

You have that cdRef as one of the private attributes on the component, which means you can spy on the component, stub that attribute and have it return whatever you want.您将cdRef作为组件的私有属性之一,这意味着您可以监视组件,存根该属性并让它返回您想要的任何内容。 Also, you can assert its calls and parameters, as needed.此外,您可以根据需要断言其调用和参数。

In your spec file, call your TestBed without providing the ChangeDetectorRef as it won't provide what you give it.在您的规范文件中,在不提供 ChangeDetectorRef 的情况下调用您的 TestBed,因为它不会提供您提供的内容。 Set the component that same beforeEach block, so it is reset between specs as it is done in the docs here :在每个块之前设置相同的组件,因此它会在规范之间重置,就像在此处的文档中所做的那样:

component = fixture.componentInstance;

Then in the tests, spy directly on the attribute然后在测试中,直接对属性进行spy

describe('someMethod()', () => {
  it('calls detect changes', () => {
    const spy = spyOn((component as any).cdRef, 'detectChanges');
    component.someMethod();

    expect(spy).toHaveBeenCalled();
  });
});

With the spy you can use .and.returnValue() and have it return whatever you need.对于间谍,您可以使用.and.returnValue()并让它返回您需要的任何内容。

Notice that (component as any) is used as cdRef is a private attribute.注意(component as any)被用作cdRef是一个私有属性。 But private doesn't exist in the actual compiled javascript so it is accessible.但是私有在实际编译的javascript中不存在,所以它是可以访问的。

It is up to you if you want to access private attributes at runtime that way for your tests.如果您想在运行时以这种方式访问​​私有属性以进行测试,这取决于您。

Not sure if this a new thing or not, but changeDetectorRef can be accessed via fixture.不确定这是否是新事物,但可以通过夹具访问 changeDetectorRef。

See docs: https://angular.io/guide/testing#componentfixture-properties请参阅文档: https : //angular.io/guide/testing#componentfixture-properties

We ran into the same issue with change detector mocking and this is ended up being the solution我们在更改检测器模拟方面遇到了同样的问题,这最终成为了解决方案

Probably one point that needs to be pointed out, is that in essence here you want to test your own code, not unit test the change detector itself (which was tested by the Angular team).可能需要指出的一点是,本质上您想在这里测试自己的代码,而不是对变更检测器本身进行单元测试(由 Angular 团队测试)。 In my opinion this is a good indicator that you should extract the call to the change detector to a local private method (private as it is something you don't want to unit test), eg在我看来,这是一个很好的指标,表明您应该将对变更检测器的调用提取到本地私有方法(私有,因为它是您不想进行单元测试的东西),例如

private detectChanges(): void {
    this.cdRef.detectChanges();
}

Then, in your unit test, you will want to verify that your code actually called this function, and thus called the method from the ChangeDetectorRef.然后,在您的单元测试中,您需要验证您的代码是否实际调用了此函数,从而调用了 ChangeDetectorRef 中的方法。 For example:例如:

it('should call the change detector',
    () => {
        const spyCDR = spyOn((cmp as any).cdRef, 'detectChanges' as any);
        cmp.ngOnInit();
        expect(spyCDR).toHaveBeenCalled();
    }
);

I had the exact same situation, and this was suggested to me as a general best practice for unit testing from a senior dev who told me that unit testing is actually forcing you by this pattern to structure your code better.我遇到了完全相同的情况,这是一位高级开发人员向我建议的单元测试的一般最佳实践,他告诉我单元测试实际上迫使您通过这种模式更好地构建代码。 With the proposed restructuring, you make sure your code is flexible to change, eg if Angular changes the way they provide us with change detection, then you will only have to adapt the detectChanges method.通过建议的重组,您可以确保您的代码可以灵活更改,例如,如果 Angular 更改了它们为我们提供更改检测的方式,那么您只需调整 detectChanges 方法。

For unit testing, if you are mocking ChangeDetectorRef just to satisfy dependency injection for a component to be creation, you can pass in any value.对于单元测试,如果您只是为了满足要创建的组件的依赖注入而ChangeDetectorRef ,则可以传入任何值。

For my case, I did this:对于我的情况,我这样做了:

TestBed.configureTestingModule({
  providers: [
    FormBuilder,
    MyComponent,
    { provide: ChangeDetectorRef, useValue: {} }
  ]
}).compileComponents()
injector = getTestBed()
myComponent = injector.get(MyComponent)

It will create myComponent successfully.它将成功创建myComponent Just make sure test execution path does not need ChangeDetectorRef .只要确保测试执行路径不需要ChangeDetectorRef If you do, then replace useValue: {} with a proper mock object.如果你这样做,那么用适当的模拟对象替换useValue: {}

In my case, I just needed to test some form creation stuff using FormBuilder .就我而言,我只需要使用FormBuilder测试一些表单创建内容。

// component
constructor(private changeDetectorRef: ChangeDetectorRef) {}

public someHandler() {
  this.changeDetectorRef.detectChanges();
}     

// spec
const changeDetectorRef = fixture.componentRef.changeDetectorRef;
jest.spyOn(changeDetectorRef, 'detectChanges');
fixture.detectChanges(); // <--- needed!!

component.someHandler();

expect(changeDetectorRef.detectChanges).toHaveBeenCalled();

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM