【问题标题】:How to stub a Typescript-Interface / Type-definition?如何存根打字稿接口/类型定义?
【发布时间】:2016-08-29 21:27:45
【问题描述】:

我在 AngularJS 1.X 项目中使用 Typescript。我将不同的 Javascript 库用于不同的目的。为了对我的源代码进行单元测试,我想使用 Typings(= 接口)来存根一些依赖项。我不想使用 ANY 类型,也不想为每个接口方法写一个空方法。

我正在寻找一种方法来做这样的事情:

let dependency = stub(IDependency);
stub(dependency.b(), () => {console.log("Hello World")});
dependency.a(); // --> Compile, do nothing, no exception
dependency.b(); // --> Compile, print "Hello World", no exception

我现在的痛苦是我要么使用any 并实现在我的测试用例中调用的所有方法,要么我实现接口并实现完整的接口。没用的代码太多了:(。

如何生成每个方法的实现都为空且类型化的对象?我使用 Sinon 进行模拟,但我也可以使用其他库。

PS:我知道 Typescript 会删除接口...但我仍然想解决这个问题:)。

【问题讨论】:

    标签: javascript angularjs typescript sinon stubbing


    【解决方案1】:

    我一直在使用 qUnit 和 Sinon 编写 Typescript 测试,我经历了与您描述的完全相同的痛苦。

    让我们假设您依赖于如下接口:

    interface IDependency {
        a(): void;
        b(): boolean;
    }
    

    通过使用基于 sinon 存根/间谍和强制转换的几种方法,我设法避免了对其他工具/库的需求。

    • 使用空对象字面量,然后直接将 sinon stubs 赋值给代码中使用的函数:

      //Create empty literal as your IDependency (usually in the common "setup" method of the test file)
      let anotherDependencyStub = <IDependency>{};
      
      //Set stubs for every method used in your code 
      anotherDependencyStub.a = sandbox.stub(); //If not used, you won't need to define it here
      anotherDependencyStub.b = sandbox.stub().returns(true); //Specific behavior for the test
      
      //Exercise code and verify expectations
      dependencyStub.a();
      ok(anotherDependencyStub.b());
      sinon.assert.calledOnce(<SinonStub>anotherDependencyStub.b);
      
    • 将对象字面量与代码所需方法的空实现一起使用,然后根据需要将方法包装在 sinon spies/stubs 中

      //Create dummy interface implementation with only the methods used in your code (usually in the common "setup" method of the test file)
      let dependencyStub = <IDependency>{
          a: () => { }, //If not used, you won't need to define it here
          b: () => { return false; }
      };
      
      //Set spies/stubs
      let bStub = sandbox.stub(dependencyStub, "b").returns(true);
      
      //Exercise code and verify expectations
      dependencyStub.a();
      ok(dependencyStub.b());
      sinon.assert.calledOnce(bStub);
      

    当您将它们与 sinon 沙箱和常见的设置/拆卸(如 qUnit 模块提供的设置/拆卸)结合使用时,它们会非常好用。

    • 在通用设置中,您可以为依赖项创建一个新沙箱和模拟对象字面量。
    • 在测试中,您只需指定间谍/存根。

    类似的事情(使用第一个选项,但如果您使用第二个选项,则工作方式相同):

    QUnit["module"]("fooModule", {
        setup: () => {
            sandbox = sinon.sandbox.create();
            dependencyMock = <IDependency>{};
        },
        teardown: () => {
            sandbox.restore();
        }
    });
    
    test("My foo test", () => {
        dependencyMock.b = sandbox.stub().returns(true);
    
        var myCodeUnderTest = new Bar(dependencyMock);
        var result = myCodeUnderTest.doSomething();
    
        equal(result, 42, "Bar.doSomething returns 42 when IDependency.b returns true");
    });
    

    我同意这仍然不是理想的解决方案,但它工作得相当好,不需要额外的库并将所需的额外代码量保持在可管理的低水平。

    【讨论】:

    • 我最近发现 @salesforce/ts-sinon 对此很有用,因为它包括一个 stubInterface 方法(以及其他方法,例如 fromStub),这使得在 TypeScript 中使用 Sinon 更好.
    【解决方案2】:

    最新的TypeMoq (ver 1.0.2) 支持 mocking TypeScript 接口,只要运行时(nodejs/browser)支持 ES6 引入的 Proxy 全局对象。

    所以,假设IDependency 看起来像这样:

    interface IDependency {
        a(): number;
        b(): string;
    }
    

    然后用 TypeMoq 模拟它就像这样简单:

    import * as TypeMoq from "typemoq";
    ...
    let mock = TypeMoq.Mock.ofType<IDependency>();
    
    mock.setup(x => x.b()).returns(() => "Hello World");
    
    expect(mock.object.a()).to.eq(undefined);
    expect(mock.object.b()).to.eq("Hello World");
    

    【讨论】:

      【解决方案3】:

      我认为简短的回答是这在 Typescript 中不可能,因为该语言不提供编译时或运行时“反射”。模拟库不可能迭代接口的成员。

      查看帖子:https://github.com/Microsoft/TypeScript/issues/1549

      这对于 TDD 开发人员来说是不幸的,因为模拟依赖项是开发工作流程的核心部分。

      但是,如其他答案所述,有许多技术可以快速存根方法。这些选项可能会完成这项工作,只需稍加调整即可。

      编辑:Typescript 抽象语法树 AST 是一种编译时“内省”——它可能用于生成模拟。不过不知道有没有人做过实用的库。

      【讨论】:

      • 这是错误的,实现类型安全的库很少,请参阅其他答案以获取一些示例。
      【解决方案4】:

      来自 npmjs:

      Mocking interfaces
      You can mock interfaces too, just instead of passing type to mock function, set mock function generic type Mocking interfaces requires Proxy implementation
      
      let mockedFoo:Foo = mock<FooInterface>(); // instead of mock(FooInterface)
      const foo: SampleGeneric<FooInterface> = instance(mockedFoo);
      

      ts-mockito 从 2.4.0 版本开始支持模拟接口:

      【讨论】:

        【解决方案5】:

        很少有库允许这样做,TypeMoqTeddyMocksTypescript-mockify 可能是更受欢迎的库之一。

        检查 github 存储库并选择您更喜欢的一个: 链接:

        您也可以使用更流行的库,例如 Sinon,但首先您必须使用 &lt;any&gt; 类型,然后将其缩小为 &lt;IDependency&gt; 类型(How do I use Sinon with Typescript?

        【讨论】:

        • 他们都需要一个类来创建一个mock,一个接口是不够的。我猜类型擦除使得它不可能,没有破解 Typescript 本身 --> stackoverflow.com/questions/13142635/…
        • 创建一个实现你的接口的空对象怎么样?并将其作为对象传递给您的模拟?
        • 那不会创建方法 --> 类型擦除 ;)
        • 对,那么唯一的解决方案就是创建一个工具来做到这一点:/
        【解决方案6】:

        你可以试试moq.ts,不过要依赖Proxy对象

        interface IDependency {
          a(): number;
          b(): string;
        }
        
        
        import {Mock, It, Times} from 'moq.ts';
        
        const mock = new Mock<IDependency>()
          .setup(instance => instance.a())
          .returns(1);
        
        mock.object().a(); //returns 1
        
        mock.verify(instance => instance.a());//pass
        mock.verify(instance => instance.b());//fail
        

        【讨论】:

          【解决方案7】:

          SafeMock 相当不错,但遗憾的是它现在似乎没有维护。 完全披露,我曾经和作者合作过。

          import SafeMock, {verify} from "safe-mock";
          
          const mock = SafeMock.build<SomeService>();
          
          // specify return values only when mocks are called with certain arguments like this
          when(mock.someMethod(123, "some arg")).return("expectedReturn");
          
          // specify thrown exceptions only when mocks are called with certain arguments like this
          when(mock.someMethod(123, "some arg")).throw(new Error("BRR! Its cold!")); 
          
          // specify that the mock returns rejected promises with a rejected value with reject
          when(mock.someMethod(123)).reject(new Error("BRR! Its cold!"));
          
          //use verify.calledWith to check the exact arguments to a mocked method
          verify(mock.someMethod).calledWith(123, "someArg");
          

          SafeMock 不会让您从模拟中返回错误的类型。

          interface SomeService {
              createSomething(): string;
          }
          
          const mock: Mock<SomeService> = SafeMock.build<SomeService>();
          
          //Won't compile createSomething returns a string
          when(mock.createSomething()).return(123); 
          

          【讨论】:

            【解决方案8】:

            现在可以了。我发布了 typescript 编译器的增强版本,它使接口元数据在运行时可用。例如,你可以写:

            interface Something {
            
            }
            
            interface SomethingElse {
                id: number;
            }
            
            interface MyService {
                simpleMethod(): void;
                doSomething(p1: number): string;
                doSomethingElse<T extends SomethingElse>(p1: Something): T;
            }
            
            function printMethods(interf: Interface) {
                let fields = interf.members.filter(m => m.type.kind === 'function'); //exclude methods.
                for(let field of fields) {
                    let method = <FunctionType>field.type;
                    console.log(`Method name: ${method.name}`);
                    for(let signature of method.signatures) {
                        //you can go really deeper here, see the api: reflection.d.ts
                        console.log(`\tSignature parameters: ${signature.parameters.length} - return type kind: ${signature.returns.kind}`);
                        if(signature.typeParameters) {
                            for(let typeParam of signature.typeParameters) {
                                console.log(`\tSignature type param: ${typeParam.name}`); //you can get constraints with typeParam.constraints
                            }
                        }
                        console.log('\t-----')
                    }
                }
            }
            
            printMethods(MyService); //now can be used as a literal!!
            

            这是输出:

            $ node main.js
            Method name: simpleMethod
                    Signature parameters: 0 - return type kind: void
                    -----
            Method name: doSomething
                    Signature parameters: 1 - return type kind: string
                    -----
            Method name: doSomethingElse
                    Signature parameters: 1 - return type kind: parameter
                    Signature type param: T
                    -----
            

            利用所有这些信息,您可以按照自己的喜好以编程方式构建存根。

            你可以找到我的项目here

            【讨论】:

            • 建议你个人的 typescript 分支并不能真正回答这个问题——人们通常认为当人们在问题中提到一种语言时,他们意味着该语言的正式发布。因此我投了反对票。
            • @Maus 这是你的意见。问题是“如何存根 Typescript-Interface / Type-definition?”。答案提供了一种方法来做到这一点。如果您阅读 github 上的官方 Typescript 问题 很多人都在尝试这样做,但团队根本不在乎,并且没有提供任何方法以干净的方式做到这一点。我证明了这种事情是可行的:如果很多人要求这个功能,也许Typescript核心团队会听取用户的请求。
            • 我认为这是令人印象深刻和重要的工作,但我仍然认为它不是这个问题的好答案
            猜你喜欢
            • 2018-02-22
            • 1970-01-01
            • 2022-08-09
            • 2017-09-26
            • 2019-01-02
            • 1970-01-01
            • 1970-01-01
            • 2017-04-13
            • 2018-12-27
            相关资源
            最近更新 更多