Typescript单元测试中的模拟(Mocking)

26
问题在于,如果对象足够复杂(在任何静态类型语言中都是如此),在Typescript中进行mock变得棘手。通常情况下,您会添加一些额外的东西来使代码编译通过,在C#中,您可以使用AutoFixture或类似工具。另一方面,JavaScript是一种动态语言,可能只需要模拟测试运行所需的对象的一部分。

因此,在Typescript单元测试中,我可以使用any类型声明我的依赖项,从而轻松地进行模拟。您是否看到这种方法的任何缺点?

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

对比

let userServiceMock: any = {
    user: {
         setting: {
             showAvatar: true
         }
    }
}
4个回答

33

从我的TypeScript单元测试经验来看,保持所有模拟对象的类型是值得的。如果你将你的模拟对象类型设置为any,那么在重命名时就会出现问题。IDE无法正确地发现应该更改哪些usersettings参数的出现情况。当然,手动编写具有完整接口的模拟对象确实非常费力。

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


5
我写了一篇文章比较了这两个库:https://medium.com/@michal.m.stocki/when-it-comes-to-mocking-in-typescript-be8531d39327 - Terite
我已经编写了处理相同问题的自己的工具,并且很愿意得到反馈意见: https://medium.com/default-to-open/unit-testing-with-angular-and-ineeda-76746a0c8f58 - phenomnomnominal
1
我使用TypeScript 3.0和ES6代理开发了一个非常棒的完全强类型库:https://www.npmjs.com/package/@fluffy-spoon/substitute - Mathias Lykkegaard Lorenzen

9
现在 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
请注意,它可以从接口创建伪造对象,并且整个过程都有完全的强类型支持!

5

正如@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 自动重构。 - Terite
另一个更好的解决方案是完全将user对象保留在服务之外。如果我们保持服务无状态,代码更容易测试。您认为让UserService在其方法中接收用户:saveUser(user:User):void;如何? - Terite
我只是举了这段代码作为例子,可能不是最好的,但并不是我试图嘲笑什么。我只是在寻找一个通用解决方案。当然,你的例子更好,谢谢建议。 - Andriy Horen
在 TypeScript 3 中,这个答案已经不再正确。请看我的回答。 - Mathias Lykkegaard Lorenzen

1
关于功能对象,您可以使用支持typescript的模拟库或具有类型定义的javascript库。无论哪种情况,类型仅在设计时间存在。 因此,jasminejs具有Spy功能,您可以以类型安全的方式使用它,如下所示:
spyOn(SomeTypescriptClass, "SomeTypescriptClassProperty");

IDE和TypeScript编译器会正确处理它。唯一的缺点是不支持参数。如果你需要对参数进行类型支持,你需要使用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 接口所替代。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接