【问题标题】:Angular 2 - Mocking Private PropertiesAngular 2 - 模拟私有属性
【发布时间】:2017-07-04 18:54:53
【问题描述】:

虽然我知道测试代码的理想方式是像在生产中一样使用它,因此不直接处理私有属性和方法 TypeScript 让我有点困惑。

我有一个用户服务。

// user.service.ts
import {Injectable} from '@angular/core';
import {AppHttpService} from '../app-http/app-http.service'
@Injectable()
export class UserService {

  constructor(private appHttp: AppHttpService) {
  }
}

如图所示,它依赖于具有私有属性和方法的 appHttp 服务,假设看起来像这样:

// app-http.service.ts
@Injectable()
export class AppHttpService {
  private apiUrl     = 'my domain';
  constructor(private http: Http, private authHttp: AuthHttp) {
  }

  post(body): Observable<any> {
     return this.http.post(this.apiUrl, body)
        .map((res)=>res)
        .catch((err)=>null);
  }
}

为了在我的用户服务上运行一个独立的测试,我想给它一个我的 appHttp 服务的简单模拟。不幸的是,如果我只是模拟 appHttp 的公共方法、属性并将其提供给我的构造函数,如下所示:

// user.service.spec.ts
describe('', () => {
  let appHttpMock = {
    post: jasmine.createSpy('post')
  };
  let service = new UserService(appHttpMock);
  beforeEach(() => {
  })
  it('', () => {
  })
})

我收到一条错误消息:

Error:(11, 33) TS2345:Argument of type '{ post: Spy; }' is not assignable to parameter of type 'AppHttpService'.  Property 'apiUrl' is missing in type '{ post: Spy; }'.

如果我将我的模拟更改为简单地添加属性,我会收到另一个错误,抱怨它不是私有的。如果我创建一个真正的模拟类,例如:

// app-http.mock.ts
export class AppHttpMockService {
  private apiUrl     = 'my domain';

  constructor() {
  }

  post() {
  }
}

我仍然会收到另一个 TypeScript 错误:

Error:(8, 33) TS2345:Argument of type 'AppHttpMockService' is not assignable to parameter of type 'AppHttpService'. Types have separate declarations of a private property 'apiUrl'.

在没有 TypeScript 对 mock 的私有属性和方法大惊小怪的情况下,什么是运行独立测试(即不需要耗时创建测试平台的测试)的干净方法?

【问题讨论】:

  • 我建议您查看官方 Angular 2 测试指南,因为您不是在测试“角度方式”。使用TestbedconfigureTestingModule 解决了这个问题。 angular.io/docs/ts/latest/guide/testing.html#!#testbed
  • 使用提供者在实例化时注入你的模拟。这就是依赖注入的目的。他们是 angular.io 关于如何做到这一点的好文档。
  • 你可以将appHttpMock的类型指定为any,强制TypeScript放松类型检查。 AFAIK,您还可以使用 jasmine.createSpyObj() 创建整个模拟对象,并假装结果是 AppHttpMockService:let appHttpMock = jasmine.createSpyObj(...) as AppHttpService;
  • 而且似乎在 jasmine d.ts 文件中声明了一个通用方法来做到这一点:jasmine.createSpyObj&lt;AppHttpService&gt;(...)
  • @Adam 谢谢,我实际上是使用带有测试台的“角度方式”编写我的集成测试(要清楚,我的意思是根据 Angular 进行集成,我在这些测试中也使用模拟作为依赖项) .不幸的是,即使使用模拟,这些测试也可能需要 500-1000 毫秒才能运行,并且如果您使用的是 TDD、BDD,则无法很好地扩展。我认为这是一种更快的不涉及 Angular 的逻辑测试方法。

标签: angular typescript


【解决方案1】:

TL;DR:如果 private 会导致您出现可测试性问题,请避免使用,因为它的作用很小。

作为 TypeScript 的忠实拥护者和该语言的拥护者,我仍然很少使用 private 关键字。

它有一些问题。

它不会限制在运行时对成员的实际访问,它仅用于限制抽象的公共 API 表面。

很明显,TypeScript 的大部分好处只是编译时间(顺便说一句,这是一件好事),所以这并不是直接反对该功能,但我觉得它给开发人员一种错误的封装感,而且更成问题的是,鼓励他们避开经过验证的真正的 JavaScript 技术,这些技术实际上通过闭包来强制保护隐私。

现在我知道,如果你使用类,通过闭包进行封装从尴尬到不可能,但如果你真的想要一个私有字段,当然如果你想要一个私有方法,你可以在类外部声明一个函数,然后不要从封闭模块中导出它。这为您提供真正的隐私。

还有另一个问题。 ECMAScript has a proposal in the works for real private properties 并且几乎可以保证使用不同的语法并具有不同的语义。

TypeScript 的 private 概念基本上语义很弱,当真正的私有属性成为 ECMAScript 的一部分时,将来会成为句法和语义冲突,所以不要在有问题的地方使用它。

我并不是说要完全避免,但在这种情况下,删除修饰符是最简单的解决方案。

还有很多可变状态,无论是私有的还是其他的,都是不可取的。有时需要进行优化,但您需要先进行衡量,以了解它是否对您的特定场景有帮助。

通常情况下,可以定义类属性的最佳对象是get,没有对应的set

【讨论】:

  • 哇!那个解释。 ??
【解决方案2】:

在这种情况下,您可以使用接口:

export interface IAppHttpService  {post(body):Observable<any>;}

export class AppHttpService implements IAppHttpService { ... } 

export class UserService {
  constructor(@Inject(AppHttpService) private appHttp: IAppHttpService) {
  }
}

【讨论】:

  • Observable&lt;any&gt; 很危险,因为它与Observable&lt;void&gt; 兼容,请考虑使用Observable&lt;{}&gt;
  • 总的来说,这是一个很好的解决方案,无论是否必要,它都说明了一种有用的技术。除了类型(易于修复 em :))之外,唯一真正的缺点是您使用具体类型作为注入实现的 DI 令牌。显然,在这种情况下这并不重要,但是使用类作为接口是一种不好的模式,这个答案的好处是它展示了如何避免这种情况,但后来却没有这样做。考虑使用这个。 export interface IAppHttpService {post(body):Observable&lt;{}&gt;;} export namespace IAppHttpService {} 然后@Inject(IAppHttpService)
  • 这需要@NgModule({ providers: [ { provide: IAppHttpService, useClass: AppHttpService } ] }) 某处。
【解决方案3】:

我有类似的问题,我找到了这个解决方案(不使用 jasmine.createSpy):

import { Service } from 'my-service';

class ServiceMock {
    service(param: string): void { // do mocking things }
}

function factoryServiceMock(): any {
    return new ServiceMock();
}

// user.service.spec.ts
describe('', () => {
    const serviceMocked = factoryServiceMock() as Service;
    const userService = new UserService(serviceMocked);
    beforeEach(() => {
    });
    it('', () => {
    });
});

我不知道这是否是最好的解决方案,但它适用于模拟策略并且显然不使用 Testbed

【讨论】:

    猜你喜欢
    • 2017-01-02
    • 1970-01-01
    • 2017-08-27
    • 2018-05-09
    • 2020-12-06
    • 1970-01-01
    • 1970-01-01
    • 2021-06-09
    • 2016-12-19
    相关资源
    最近更新 更多