测试 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 用 Ja​​smine 测试 Angular 就像这个答案