测试 Angular 服务是否已经初始化
Test that Angular service have been initialized
我正在尝试使用 Karma-Jasmine 测试我的 Angular 服务,我需要确保在服务初始化后调用了 loadApp
函数。最好的测试方法是什么?
import { Injectable, NgZone } from '@angular/core';
@Injectable()
export class GdlService {
appName = 'myAppName';
constructor(
private ngZone: NgZone,
) {
this.ngZone = ngZone;
this.loadApp(this.appName);
}
private loadApp(appName) {
this.ngZone.runOutsideAngular(() => {
// ...some logic
});
}
}
它可以像任何其他功能一样进行测试。考虑到 loadApp
是原型方法,可以在 class 原型上对其进行存根或监视:
it('', () => {
spyOn(<any>GdlService.prototype, 'loadApp');
const gdl = TestBed.get(GdlService);
expect(gdl['loadApp']).toHaveBeenCalledWith('myAppName');
});
尝试模拟 ngZone 的注入(我喜欢 ts-mockito 这类东西),然后检查是否已调用 ngZone.outsideOfAngular。由于 typescript 的性质,我认为您无法直接轻松地监视任何私人内容。
测试文件中类似这样的内容:
import { GdlService } from 'place';
import { NgZone } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
anything,
instance,
mock,
verify
} from 'ts-mockito';
describe('yada yada', () => {
const mockNgZone = mock(NgZone);
// Can use when(mockNgZone.whatever)... to mock what you need
beforeEach(() => {
TestBed.configureTestModule({
providers: [{
provide: NgZone,
useValue: instance(mockNgZone)
}]
});
});
it('checks on loadApp', () => {
verify(mockNgZone.runOutsideAngular(anything())).called();
});
});
如果您只想使用 spyOn 方法,您可以只替换提供程序的 useValue 部分中的对象。
出于测试目的增加成员的可见性是可以的。因此,为了优雅起见,您可能希望将 loadApp public 用于模拟。然而,尝试模拟一个私有函数会带来一些权衡。 @estus 的回答是正确的:
我稍微调整了一下,使用 jasmine.createSpy 修改原型来覆盖私有函数。
it('try to call loadApp', () => {
GdlService.prototype['loadApp'] = jasmine.createSpy()
.and
.callFake((appName) => {
console.log('loadApp called with ' , appName );
});
// spyOn(IEFUserService.prototype, 'loadAppPrivate'); - this does not work because the test breaks right here trying to access private member
const service = TestBed.get(GdlService);
expect(service['loadApp']).toHaveBeenCalled();
});
测试构造函数中的私有方法调用
Isolated unit tests are considered best practice when testing a service by the Angular Testing Guide,即不需要 Angular 测试实用程序。
我们无法通过监视对象实例来测试是否从构造函数调用了方法,因为一旦我们引用了实例,就已经调用了该方法。
相反,我们需要监视服务的原型 (thanks, Dave Newton!)。在 JavaScript class
上创建方法时,我们实际上是在 <ClassName>.prototype
.
上创建方法
鉴于此工厂是基于 MockNgZone from Angular testing internals:
的 NgZone 间谍
import { EventEmitter, NgZone } from '@angular/core';
export function createNgZoneSpy(): NgZone {
const spy = jasmine.createSpyObj('ngZoneSpy', {
onStable: new EventEmitter(false),
run: (fn: Function) => fn(),
runOutsideAngular: (fn: Function) => fn(),
simulateZoneExit: () => { this.onStable.emit(null); },
});
return spy;
}
我们可以模拟 NgZone 依赖项来隔离我们测试中的服务,甚至可以描述 运行 区域外的传出命令。
// Straight Jasmine - no imports from Angular test libraries
import { NgZone } from '@angular/core';
import { createNgZoneSpy } from '../test/ng-zone-spy';
import { GdlService } from './gdl.service';
describe('GdlService (isolated unit tests)', () => {
describe('loadApp', () => {
const methodUnderTest: string = 'loadApp';
let ngZone: NgZone;
let service: GdlService;
beforeEach(() => {
spyOn<any>(GdlService.prototype, methodUnderTest).and.callThrough();
ngZone = createNgZoneSpy();
service = new GdlService(ngZone);
});
it('loads the app once when initialized', () => {
expect(GdlService.prototype[methodUnderTest]).toHaveBeenCalledWith(service.appName);
expect(GdlService.prototype[methodUnderTest]).toHaveBeenCalledTimes(1);
});
it('runs logic outside the zone when initialized.', () => {
expect(ngZone.runOutsideAngular).toHaveBeenCalledTimes(1);
});
});
});
通常我们不想测试私有方法,而是观察它产生的 public 副作用。
但是,我们可以使用Jasmine Spies来实现我们想要的。
See full example on StackBlitz
Angular 服务生命周期
See examples that demonstrate the Angular Service Lifecycle on StackBlitz。请务必阅读 hello.*.ts
文件中的注释并打开 JavaScript 控制台以查看输出的消息。
使用 Jasmine 测试创建 Angular StackBlitz
Fork my StackBlitz 用 Jasmine 测试 Angular 就像这个答案
我正在尝试使用 Karma-Jasmine 测试我的 Angular 服务,我需要确保在服务初始化后调用了 loadApp
函数。最好的测试方法是什么?
import { Injectable, NgZone } from '@angular/core';
@Injectable()
export class GdlService {
appName = 'myAppName';
constructor(
private ngZone: NgZone,
) {
this.ngZone = ngZone;
this.loadApp(this.appName);
}
private loadApp(appName) {
this.ngZone.runOutsideAngular(() => {
// ...some logic
});
}
}
它可以像任何其他功能一样进行测试。考虑到 loadApp
是原型方法,可以在 class 原型上对其进行存根或监视:
it('', () => {
spyOn(<any>GdlService.prototype, 'loadApp');
const gdl = TestBed.get(GdlService);
expect(gdl['loadApp']).toHaveBeenCalledWith('myAppName');
});
尝试模拟 ngZone 的注入(我喜欢 ts-mockito 这类东西),然后检查是否已调用 ngZone.outsideOfAngular。由于 typescript 的性质,我认为您无法直接轻松地监视任何私人内容。
测试文件中类似这样的内容:
import { GdlService } from 'place';
import { NgZone } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
anything,
instance,
mock,
verify
} from 'ts-mockito';
describe('yada yada', () => {
const mockNgZone = mock(NgZone);
// Can use when(mockNgZone.whatever)... to mock what you need
beforeEach(() => {
TestBed.configureTestModule({
providers: [{
provide: NgZone,
useValue: instance(mockNgZone)
}]
});
});
it('checks on loadApp', () => {
verify(mockNgZone.runOutsideAngular(anything())).called();
});
});
如果您只想使用 spyOn 方法,您可以只替换提供程序的 useValue 部分中的对象。
出于测试目的增加成员的可见性是可以的。因此,为了优雅起见,您可能希望将 loadApp public 用于模拟。然而,尝试模拟一个私有函数会带来一些权衡。 @estus 的回答是正确的:
我稍微调整了一下,使用 jasmine.createSpy 修改原型来覆盖私有函数。
it('try to call loadApp', () => {
GdlService.prototype['loadApp'] = jasmine.createSpy()
.and
.callFake((appName) => {
console.log('loadApp called with ' , appName );
});
// spyOn(IEFUserService.prototype, 'loadAppPrivate'); - this does not work because the test breaks right here trying to access private member
const service = TestBed.get(GdlService);
expect(service['loadApp']).toHaveBeenCalled();
});
测试构造函数中的私有方法调用
Isolated unit tests are considered best practice when testing a service by the Angular Testing Guide,即不需要 Angular 测试实用程序。
我们无法通过监视对象实例来测试是否从构造函数调用了方法,因为一旦我们引用了实例,就已经调用了该方法。
相反,我们需要监视服务的原型 (thanks, Dave Newton!)。在 JavaScript class
上创建方法时,我们实际上是在 <ClassName>.prototype
.
鉴于此工厂是基于 MockNgZone from Angular testing internals:
的 NgZone 间谍import { EventEmitter, NgZone } from '@angular/core';
export function createNgZoneSpy(): NgZone {
const spy = jasmine.createSpyObj('ngZoneSpy', {
onStable: new EventEmitter(false),
run: (fn: Function) => fn(),
runOutsideAngular: (fn: Function) => fn(),
simulateZoneExit: () => { this.onStable.emit(null); },
});
return spy;
}
我们可以模拟 NgZone 依赖项来隔离我们测试中的服务,甚至可以描述 运行 区域外的传出命令。
// Straight Jasmine - no imports from Angular test libraries
import { NgZone } from '@angular/core';
import { createNgZoneSpy } from '../test/ng-zone-spy';
import { GdlService } from './gdl.service';
describe('GdlService (isolated unit tests)', () => {
describe('loadApp', () => {
const methodUnderTest: string = 'loadApp';
let ngZone: NgZone;
let service: GdlService;
beforeEach(() => {
spyOn<any>(GdlService.prototype, methodUnderTest).and.callThrough();
ngZone = createNgZoneSpy();
service = new GdlService(ngZone);
});
it('loads the app once when initialized', () => {
expect(GdlService.prototype[methodUnderTest]).toHaveBeenCalledWith(service.appName);
expect(GdlService.prototype[methodUnderTest]).toHaveBeenCalledTimes(1);
});
it('runs logic outside the zone when initialized.', () => {
expect(ngZone.runOutsideAngular).toHaveBeenCalledTimes(1);
});
});
});
通常我们不想测试私有方法,而是观察它产生的 public 副作用。
但是,我们可以使用Jasmine Spies来实现我们想要的。
See full example on StackBlitz
Angular 服务生命周期
See examples that demonstrate the Angular Service Lifecycle on StackBlitz。请务必阅读 hello.*.ts
文件中的注释并打开 JavaScript 控制台以查看输出的消息。
使用 Jasmine 测试创建 Angular StackBlitz
Fork my StackBlitz 用 Jasmine 测试 Angular 就像这个答案