【问题标题】:Mocking in Typescript unit tests在 Typescript 单元测试中模拟
【发布时间】: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
         }
    }
}

【问题讨论】:

    标签: javascript unit-testing typescript mocking


    【解决方案1】:

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

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

    【讨论】:

    【解决方案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 接口。

      【讨论】:

        猜你喜欢
        • 2019-12-13
        • 2019-10-25
        • 2016-08-02
        • 2018-11-27
        • 1970-01-01
        • 2015-05-04
        • 2016-11-14
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多