在 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 不会正确发现应该更改哪些 user
或 settings
参数。当然手动写一个完整的接口的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 接口导致模拟对象出错