如何存根打字稿 - 接口/类型定义?

时间:2016-05-04 12:24:55

标签: javascript angularjs typescript sinon stubbing

我在AngularJS 1.X项目中使用Typescript。我使用不同的Javascript库用于不同的目的。要对我的源进行单元测试,我想使用Typings(= interfaces)来存根一些依赖项。我不想使用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会删除接口......但我仍想解决这个问题:)。

8 个答案:

答案 0 :(得分:22)

我一直在使用qUnit和Sinon编写Typescript测试,而且我遇到了与你描述的完全相同的痛苦。

假设您依赖于以下界面:

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

我已经设法通过使用基于sinon存根/间谍和投射的几种方法来避免使用额外的工具/库。

  • 使用空对象文字,然后直接将sinon存根分配给代码中使用的函数:

    //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间谍/存根中

    //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");
});

我同意这仍然不是理想的解决方案,但它运作得相当好,不需要额外的库,并且需要额外的代码量以达到低可管理水平。

答案 1 :(得分:13)

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

所以,假设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"); 看起来像这样:

 class CustomAdapter extends ArrayAdapter<CharSequence>{

    public CustomAdapter(Context context, CharSequence[] routes) {
        super(context, R.layout.custom_row ,routes);
    }

@NonNull
@Override
public View getView(int position, View convertView, ViewGroup parent) {

    LayoutInflater routeInflater = LayoutInflater.from(getContext());
    View customView = convertView;
    if(customView == null){customView = routeInflater.inflate(R.layout.custom_row, parent, false);}

    CharSequence singleRoute = getItem(position);
    TextView routeText = (TextView) customView.findViewById(R.id.routeText);
    routeText.setText(singleRoute);

    ///// Textview I want to add
    CharSequence routeNum = getItem(position);
    TextView routeNumText = (TextView) customView.findViewById(R.id.numbersTextView);
    routeNumText.setText(routeNum);
    /////
    return customView;

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

 ///// fill listview numbers I want to add
    final String[] routeListviewNumbers = getResources().getStringArray(R.array.routeNumbers);
    //fill list view with xml array of routes
    final String[] routeListViewItems = getResources().getStringArray(R.array.routeList);

   //custom adapter for list view
    ListAdapter routeAdapter = new CustomAdapter(this, routeListViewItems);
    final ListView routeListView = (ListView) findViewById(R.id.routeListView);
    routeListView.setAdapter(routeAdapter);

答案 2 :(得分:8)

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

见线程:https://github.com/Microsoft/TypeScript/issues/1549

对于TDD开发人员而言,这是不幸的,其中模拟依赖项是开发工作流程的核心部分。

然而,正如其他答案所描述的那样,有许多技术可以快速存根。这些选项可以通过一些心理调整来完成工作。

答案 3 :(得分:2)

很少有图书馆允许这样做TypeMoqTeddyMocksTypescript-mockify可能是最受欢迎的图书馆之一。

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

您还可以使用像Sinon这样的更受欢迎的库,但首先您必须使用<any>类型,然后将其缩小为<IDependency>类型(How do I use Sinon with Typescript?

答案 4 :(得分:2)

现在可能。我发布了一个增强版的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

答案 5 :(得分:2)

来自 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 版本开始支持模拟接口:

答案 6 :(得分:0)

您可以尝试moq.ts,但这取决于代理对象

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 :(得分:0)

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);