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

Angular 2: How to mock ChangeDetectorRef while unit testing

我刚刚开始进行单元测试,我已经能够模拟我自己的服务以及一些 Angular 和 Ionic,但无论我做什么 ChangeDetectorRef一样。

我是说这是什么魔法?

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

 it('fails no matter what', async(() => {
    spyOn(cdRef, 'markForCheck');
    spyOn(cmp.cdRef, 'markForCheck');

    cmp.ngOnInit();

    expect(cdRef.markForCheck).toHaveBeenCalled();  // fail, why ??
    expect(cmp.cdRef.markForCheck).toHaveBeenCalled(); // success

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

@Component({
  ...
})
export class MyComponent {
 constructor(private cdRef: ChangeDetectorRef){}

 ngOnInit() {
   // do something
   this.cdRef.markForCheck();
 }
}

我什么都试过了,asyncfakeAsyncinjector([ChangeDetectorRef], () => {})

没有任何效果。

2020 年更新

我最初是在 2017 年 5 月写的,这个解决方案在当时非常有效,现在仍然有效。

我们无法通过测试床配置 changeDetectorRef mock 的注入,所以这就是我最近在做的事情:

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

那你就不用关心私有属性什么的了。


万一有人遇到这个问题,这是一种对我很有效的方法:

当您在构造函数中注入 ChangeDetectorRef 实例时:

 constructor(private cdRef: ChangeDetectorRef) { }

您将 cdRef 作为组件的私有属性之一,这意味着您可以监视组件,存根该属性并根据需要使用它 return。此外,您可以根据需要声明其调用和参数。

在您的规范文件中,在不提供 ChangeDetectorRef 的情况下调用您的 TestBed,因为它不会提供您提供的内容。在每个块之前设置相同的组件,因此它在规范之间重置,就像在文档中所做的那样 here:

component = fixture.componentInstance;

然后在测试中,直接监视属性

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

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

有了间谍,您可以使用 .and.returnValue() 并return 随心所欲。

请注意,使用 (component as any) 是因为 cdRef 是私有属性。但是private在实际编译的时候不存在javascript所以可以访问。

如果您想在运行时以这种方式访问​​私有属性以进行测试,则由您决定。

可能需要指出的一点是,本质上您要在这里测试自己的代码,而不是对变更检测器本身进行单元测试(由 Angular 团队测试)。 在我看来,这是一个很好的指标,表明您应该将对更改检测器的调用提取到本地私有方法(私有,因为它是您不想进行单元测试的东西),例如

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

然后,在您的单元测试中,您需要验证您的代码是否实际调用了此函数,从而调用了 ChangeDetectorRef 中的方法。例如:

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

我遇到了完全相同的情况,这是一位高级开发人员向我建议的单元测试的一般最佳实践,他告诉我单元测试实际上是通过这种模式迫使您更好地构建代码。通过建议的重组,您可以确保您的代码可以灵活更改,例如如果 Angular 改变了他们为我们提供变化检测的方式,那么您只需调整 detectChanges 方法。

对于单元测试,如果您模拟 ChangeDetectorRef 只是为了满足要创建的组件的依赖注入,您可以传入任何值。

对于我的情况,我是这样做的:

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

它将创建 myComponent 成功。只需确保测试执行路径不需要 ChangeDetectorRef。如果这样做,请将 useValue: {} 替换为适当的模拟对象。

就我而言,我只需要使用 FormBuilder.

测试一些表单创建内容

不确定这是否是新事物,但是可以通过 fixture 访问 changeDetectorRef。

查看文档:https://angular.io/guide/testing#componentfixture-properties

我们 运行 遇到了与更改检测器模拟相同的问题,这最终成为解决方案

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