Как заглушить 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
Ответ 7
Похоже, что ts-mockito также поддерживает интерфейсы mocking начиная с версии 2.4.0: https://github.com/NagRock/ts-mockito/releases/tag/v2.4.0
Ответ 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);