在 Typescript 单元测试中模拟

Posted

技术标签:

【中文标题】在 Typescript 单元测试中模拟【英文标题】:Mocking in Typescript unit tests 【发布时间】:2017-02-09 01:40:03 【问题描述】:

问题在于,如果对象足够复杂(在任何强类型语言中都是如此),Typescript 中的模拟会变得很棘手。你通常会模拟一些额外的东西来编译代码,例如在 C# 中,你可以使用 AutoFixture 或类似的东西。另一方面,javascript 是动态语言,它可以只模拟运行测试所需的对象的一部分。

所以在 Typescript 单元测试中,我可以使用 any 类型声明我的依赖项,从而轻松模拟它。你看到这种方法有什么缺点吗?

let userServiceMock: MyApp.Services.UserService = 
    // lots of thing to mock

let userServiceMock: any = 
    user: 
         setting: 
             showAvatar: true
         
    

【问题讨论】:

【参考方案1】:

我在 TypeScript 中进行单元测试的经验明确表明,保持所有模拟对象的类型是值得的。当您使用 any 类型离开模拟时,在重命名期间会出现问题。 IDE 不会正确发现应该更改哪些 usersettings 参数。当然手动写一个完整的接口的mock对象真的很费力。

幸运的是,TypeScript 有两个工具可以创建类型安全的模拟对象:ts-mockito(受Java mockito 启发)和typemoq(受C# Moq 启发)。

【讨论】:

我写了一篇比较两个库的文章:medium.com/@michal.m.stocki/… 我已经编写了自己的工具来处理同样的问题,我希望得到一些反馈:medium.com/default-to-open/… 我为此使用 TypeScript 3.0 和 ES6 代理开发了一个非常棒的全强类型库:npmjs.com/package/@fluffy-spoon/substitute【参考方案2】:

现在 TypeScript 3 出来了,终于可以表达完整的强类型了!我利用了这一点并将 NSubstitute 移植到了 TypeScript。

可以在这里找到:https://www.npmjs.com/package/@fluffy-spoon/substitute

我在这里与最流行的框架进行了比较:https://medium.com/@mathiaslykkegaardlorenzen/with-typescript-3-and-substitute-js-you-are-already-missing-out-when-mocking-or-faking-a3b3240c4607

注意它如何从接口创建假货,并在此过程中拥有完整的强类型!

【讨论】:

【参考方案3】:

正如@Terite any 所指出的,模拟上的类型是不好的选择,因为模拟与其实际类型/实现之间没有关系。因此改进的解决方案可能是将部分模拟的对象转换为模拟类型:

export interface UserService 
    getUser: (id: number) => User;
    saveUser: (user: User) => void;
    // ... number of other methods / fields

.......

let userServiceMock: UserService = <UserService> 
    saveUser(user: User)  console.log("save user"); 

spyOn(userServiceMock, 'getUser').andReturn(new User());
expect(userServiceMock.getUser).toHaveBeenCalledWith(expectedUserId);

还值得一提的是,Typescript 不允许强制转换任何具有额外成员(超集或派生类型)的对象。意味着您的部分模拟实际上是UserService 的基本类型,并且可以安全地转换。例如

// Error: Neither type '...' nor 'UserService' is assignable to the other.
let userServiceMock: UserService = <UserService> 
     saveUser(user: User)  console.log("save user"); ,
     extraFunc: () =>   // not available in UserService

【讨论】:

为什么,拥有部分模拟对象对您来说如此重要?在同一个服务中拥有公共字段和公共方法是相当糟糕的主意。最好定义方法setAvatarVisibility(visible:boolean) 并保持user 字段私有。然后,您将避免手动监视。请注意,在spyOn(userServiceMock, 'saveUser'); 中,方法的字符串名称不会被 IDE 自动重构。 另一个更好的解决方案是将user 对象完全保留在服务之外。如果我们保持服务无状态,代码更容易测试。您如何看待让UserService 接收用户到它的方法:saveUser(user:User):void; 我仅将此代码用作示例,可能不是最好的,但它不是我试图模拟的东西。我只是在寻找一个通用的解决方案。是的,你的例子当然更好,谢谢你的建议。 这个答案在 TypeScript 3 中不再适用。请参阅我的答案。【参考方案4】:

对于功能对象,您可以使用支持 typescript 的模拟库或具有类型定义的 javascript 库。在这两种情况下,类型只存在于设计时。 所以 jasminesjs 具有 Spy 功能,您可以像这样以类型安全的方式使用它:

spyOn(SomeTypescriptClass, "SomeTypescriptClassProperty");

IDE 和 typescript 编译器会正确处理它。唯一的缺点是不支持参数。如果您需要对参数的类型支持,则需要使用 typescript 模拟库。我可以为打字稿添加另一个模拟库moq.ts

至于 DTO 对象,您可以使用这种方法:

export type IDataMock<T> = 
  [P in keyof T]?: IDataMock<T[P]>;
;

export function dataMock<T>(instance: IDataMock<T>): T 
  return instance as any;


// so where you need
const obj = dataMock<SomeBigType>(onlyOneProperty: "some value");

我记得 IDataMock 可以替换为 typescript 中的标准 Partial 接口。

【讨论】:

以上是关于在 Typescript 单元测试中模拟的主要内容,如果未能解决你的问题,请参考以下文章

在 Angular 11 中进行单元测试时,将 json 对象转换为 typescript 接口导致模拟对象出错

在单元测试用例中模拟 Angular $window

Jest Manual Mocks with React 和 Typescript:模拟 ES6 类依赖

TypeScript:仅在单元测试中找不到模块的声明文件

如何在 Typescript 中对私有方法进行单元测试

TypeScript - 使用带有单元测试的模块或类?