Как заглушить Typescript -Interface/Type-definition?

Я работаю с Typescript в проекте AngularJS 1.X. Я использую разные библиотеки 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 стирает интерфейсы... но я все равно хотел бы решить это:).

Ответ 1

Я думаю, что короткий ответ заключается в том, что это невозможно в Typescript, так как язык не предлагает "отражения" во время компиляции или во время выполнения. Для фиктивной библиотеки невозможно выполнить итерации членов интерфейса.

Смотрите тему: https://github.com/Microsoft/TypeScript/issues/1549

Это печально для разработчиков TDD, в которых насмешка над зависимостями является центральной частью рабочего процесса разработки.

Тем не менее, существует ряд приемов для быстрого определения методов, как описано в других ответах. Эти варианты могут сделать работу, с небольшим умственным приспособлением.

Редактировать: Абстрактное синтаксическое дерево Typescript, AST, является "самоанализом" времени компиляции, которое, вероятно, можно использовать для генерации макетов. Однако я не знаю, создал ли кто-нибудь практическую библиотеку.

Ответ 2

Я писал тесты Typescript с использованием qUnit и Sinon, и я испытал ту же боль, о которой вы описываете.

Предположим, что у вас есть зависимость от интерфейса, например:

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

Мне удалось избежать необходимости в дополнительных инструментах/библиотеках, используя пару подходов, основанных на синусных заглушках/шпионах и литье.

  • Используйте пустой литерал объекта, затем прямо назначьте блоки синонов функциям, используемым в коде:

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

Я бы согласился, что это еще не идеальное решение, но оно работает достаточно хорошо, не требует дополнительных библиотек и не требует дополнительного кода, необходимого для низкого уровня управления.

Ответ 3

Последний TypeMoq (версия 1.0.2) поддерживает насмешливые интерфейсы TypeScript, если поддерживается среда выполнения (nodejs/browser) глобальный объект Proxy, введенный ES6.

Итак, если 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");

Ответ 4

Существует несколько библиотек, которые позволяют сделать это TypeMoq, TeddyMocks и Typescript-mockify, вероятно, одним из наиболее популярных.

Проверьте репозитории github и выберите тот, который вам больше нравится: Ссылки по теме:

Вы также можете использовать более популярные библиотеки, такие как Sinon, но сначала вы должны использовать тип <any>, а затем сузить его до <IDependency> type (Как использовать Sinon с Typescript?)

Ответ 5

Теперь это возможно. Я выпустил улучшенную версию компилятора 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
        -----

Со всей этой информацией вы можете создавать заглушки программно, как вы предпочитаете.

Вы можете найти мой проект здесь.

Ответ 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

Ответ 8

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