【问题标题】:Test that Angular service have been initialized测试 Angular 服务是否已初始化
【发布时间】:2017-12-29 12:02:25
【问题描述】:

我正在尝试使用 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
    });
  }
}

【问题讨论】:

  • 不要。你不应该对私有行为进行单元测试,当然也不应该模拟私有方法。
  • @Pace 为什么?它是私有的这一事实意味着它不属于公共接口。它与测试方法无关。
  • 它是admittedly controversial,但我认为实用单元测试说得最好,“大多数时候,您应该能够通过执行其公共方法来测试一个类。如果私有或受保护的访问背后隐藏着重要的功能,这可能是一个警告信号,表明那里有另一个班级正在努力摆脱困境。”我还没有在自己的工作中发现异常。
  • 你应该能够通过执行它的公共方法来测试一个类,是的,这就是重点。如果没有私人成员就无法做到这一点,这意味着设计出了问题。访问私人成员应该可以加强测试 - 并以一种好的方式使它们变得脆弱。跟踪间谍调用的痕迹可能会大大加快失败测试中问题的解决速度。

标签: angular typescript karma-runner karma-jasmine


【解决方案1】:

它可以像任何其他功能一样进行测试。考虑到loadApp 是原型方法,它可以被存根或监视类原型:

it('', () => {
  spyOn(<any>GdlService.prototype, 'loadApp');
  const gdl = TestBed.get(GdlService);
  expect(gdl['loadApp']).toHaveBeenCalledWith('myAppName');
});

【讨论】:

  • 嗨!谢谢,我试过了,但得到了Expected spy loadApp to have been called
  • 使用toHaveBeenCalledWith 会抛出Expected spy loadApp to have been called with [ 'myAppName' ] but it was never called.
  • TestBed.get(GdlService) 应该实例化服务并调用方法。如果之前已实例化(在 beforeEach inject 或其他中),则测试将失败。鉴于GdlService 没有在TestBed.get(GdlService) 之前实例化,我希望上面的代码可以工作。
  • 我使用完全相同的订单,没有任何更改,我得到 Argument of type '"loadApp"' is not assignable to parameter of type '"sendEvent" | "appName"'. 是我服务的另一个私有方法,appName 是一个包含应用名称的字符串变量
  • 使用spyOn&lt;any&gt;(GdlService.prototype, 'loadApp'); 清除错误。 loadApp 方法是私有的。 spyOn 使用 keyof 来防止拼写错误,但 keyof 不适用于私人成员。
【解决方案2】:

尝试模拟 ngZone 的注入(我喜欢 ts-mockito 这类东西),然后检查是否调用了 ngZone.outsideOfAngular。由于打字稿的性质,我认为您无法轻松地直接窥探任何私密的内容。

测试文件中有这样的内容:

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 部分中的对象。

【讨论】:

  • 我认为您无法直接监视任何私密的内容 - 这当然是可能的。这被认为是反射,有几种公认的方法可以做到这一点。
  • 任何私密的舒适。我修改了我的答案以反映这一点。
  • gdl['loadApp'](&lt;any&gt;gdl).loadApp 在不用于黑客攻击而是用于反射目的时是舒适且完全有效的方式。 Reflect.get(gdl, 'loadApp')more cumbersome but also descriptive way to do that
  • 做这样的事情通常会失去对打字系统的所有保护,而且有点脆弱。当所有其他人都筋疲力尽时,寻求反思应该是最后的选择。
  • the comment。是的,它当然很脆弱,但我认为这是一件好事。过于严格的红色测试比过于松散的绿色测试要好得多。这是一种选择,而不是应该做出的选择。 ngZone 应该被监视/存根的答案是正确的。我认为断言 both loadApp 和 runOutsideAngular 调用是双赢的局面。如果出现问题,这可以从没有调试的失败断言中推断出来。
【解决方案3】:

为了测试目的增加成员的可见性是可以的。因此,为了优雅起见,您可能希望将 loadApp 公开以进行模拟。然而,试图模拟一个私有函数会带来一些折衷。 @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();
  });

【讨论】:

  • GdlService.prototype['loadApp'] = 是监视或存根方法的危险方法。它不会在测试后被清理,而spyOn 会。需要保存原始方法并在afterEach中手动恢复
  • 但是给定私有方法,其他变体会给出错误,例如spyOn(&lt;any&gt;GdlService.prototype, 'loadApp');。另一种选择当然是将 loadApp 公开。
  • spyOn(&lt;any&gt;GdlService.prototype, 'loadApp') 不应该给出错误。预计会起作用,但没有来自 OP 的反馈。
  • 你是对的。它不会产生编译错误。
【解决方案4】:

在构造函数中测试私有方法调用

Isolated unit tests 被视为best practice when testing a service by the Angular Testing Guide,即不需要 Angular 测试实用程序。

我们无法通过监视对象实例来测试构造函数是否调用了方法,因为一旦我们引用了该实例,该方法就已经被调用了。

相反,我们需要监视服务的原型 (thanks, Dave Newton!)。在 JavaScript class 上创建方法时,我们实际上是在 &lt;ClassName&gt;.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);
    });
  });
});

通常我们不想测试私有方法,而是观察它产生的公共副作用。

但是,我们可以使用Jasmine Spies 来实现我们想要的。

See full example on StackBlitz

Angular 服务生命周期

See examples that demonstrate the Angular Service Lifecycle on StackBlitz。请阅读 hello.*.ts 文件中的 cmets 并打开 JavaScript 控制台以查看输出的消息。

使用 Jasmine 测试创建 Angular StackBlitz

Fork my StackBlitz 用 Ja​​smine 测试 Angular,就像在这个答案中一样

【讨论】:

    猜你喜欢
    • 2013-09-20
    • 2015-09-15
    • 1970-01-01
    • 2022-07-05
    • 2015-12-04
    • 2011-06-29
    • 2014-08-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多