Angular 2:如何在单元测试时模拟 ChangeDetectorRef
Posted
技术标签:
【中文标题】Angular 2:如何在单元测试时模拟 ChangeDetectorRef【英文标题】:Angular 2: How to mock ChangeDetectorRef while unit testing 【发布时间】:2017-05-16 06:20:24 【问题描述】:我刚刚开始使用单元测试,我已经能够模拟我自己的服务以及一些 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();
我什么都试过了,async
,fakeAsync
,injector([ChangeDetectorRef], () => )
。
没有任何作用。
【问题讨论】:
Angular 2 编译器对 ChangeDetectorRef 进行了特殊处理。我认为你不能提供它。您可以检查 AsyncPipe github.com/angular/angular/blob/… 的测试有使用的 SpyChangeDetectorRef 我遇到了同样的问题 - 人们是如何解决这个问题的? 【参考方案1】: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
作为组件的私有属性之一,这意味着您可以监视组件,存根该属性并让它返回您想要的任何内容。此外,您可以根据需要断言其调用和参数。
在您的规范文件中,调用您的 TestBed 而不提供 ChangeDetectorRef,因为它不会提供您提供的内容。在每个块之前设置相同的组件,以便在规范之间重置它,就像在文档 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()
并让它返回你需要的任何东西。
注意(component as any)
被用作cdRef
是一个私有属性。但是私有在实际编译的javascript中不存在,所以可以访问。
如果您想在运行时以这种方式访问私有属性以进行测试,这取决于您。
【讨论】:
那么,当您不将私有字段视为私有字段时,为什么还要创建它们呢?大声笑 原始问题被声明为私有。我刚刚回答了 ;) 有关于“我应该测试私有方法”的旧讨论,其中有两种说法,是和否。两者都有正当理由。我的建议是选择让你快乐的人。 @Juan 为什么ChangeDetectorRef
不能在TestBed
中提供,为什么要访问私有字段?
@mbharanidharan88 那是 2017 年 5 月,当时我遇到了问题。我刚刚在其他答案中添加了推荐测试床的答案,而不关心内部/私人细节。
我更新了。那里有很好的答案,显示了使用模拟的最后一块,所以现在这个更新将得到尽可能详细的信息,因为这是一个古老的问题和答案,并且那里有很好的信息。我希望它现在为您指明正确的方向@mbharanidharan88【参考方案2】:
可能需要指出的一点是,本质上您想测试自己的代码,而不是对变更检测器本身进行单元测试(由 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
并检查toHaveBeenCalled
而不是监视Component.ChangeDetectorRef
?【参考方案3】:
对于单元测试,如果您模拟 ChangeDetectorRef
只是为了满足要创建的组件的依赖注入,您可以传入任何值。
就我而言,我这样做了:
TestBed.configureTestingModule(
providers: [
FormBuilder,
MyComponent,
provide: ChangeDetectorRef, useValue:
]
).compileComponents()
injector = getTestBed()
myComponent = injector.get(MyComponent)
它将成功创建myComponent
。只要确保测试执行路径不需要ChangeDetectorRef
。如果你这样做了,请将useValue:
替换为适当的模拟对象。
就我而言,我只需要使用FormBuilder
测试一些表单创建内容。
【讨论】:
ChangeDetectorRef
不是通过 DI 提供的,所以不能提供双精度。【参考方案4】:
不确定这是否是新事物,但可以通过夹具访问 changeDetectorRef。
参见文档:https://angular.io/guide/testing#componentfixture-properties
我们在更改检测器模拟中遇到了同样的问题,这最终成为了解决方案
【讨论】:
这是要走的路。这比监视组件的属性要好得多,尤其是私有属性。谢谢!fixture.changeDetectorRef
与提供的组件不一样,所以不能使用。【参考方案5】:
// 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();
【讨论】:
以上是关于Angular 2:如何在单元测试时模拟 ChangeDetectorRef的主要内容,如果未能解决你的问题,请参考以下文章
用玩笑进行单元测试时,如何以角度模拟 ResizeObserver polyfill?