Typescript - 具有映射类型的Mock接口

时间:2018-05-07 15:44:29

标签: typescript testing

出于测试目的,我希望能够创建一个实现接口的对象,只有我测试所需的功能,而不必手动维护具有所有可能属性的模拟对象。一般来说,我一次只使用一个函数,所以不需要定义所有其他函数,但我不希望TS继续抱怨缺少属性。

例如,我有一个界面IFoo

interface IFoo {
    myFunc(): string;
    otherFunc(): number;
}

我尝试创建一个映射类型,将jest.Mock<{}>分配给IFoo的所有属性

type Mockify<T> = {
    [P in keyof T]: jest.Mock<{}>
};

这样称呼:

const mockFoo: Mockify<IFoo> = {
    otherFunc: {
        // Mocked function behavior
    }
}

这种方法的问题是TS抱怨传递给Mockify的对象缺少myFunc属性。

我还尝试Mockify<Partial<IFoo>>忽略遗漏的属性,但我的类型定义与IFoo不同,而IFoo的其他一些函数正在抱怨。 我也可以将所有属性定义为optionnal,但从概念上讲我不喜欢它。

我觉得映射类型可以完成工作,但我可能没有正确的方法。

感谢您的帮助!

2 个答案:

答案 0 :(得分:0)

我可以在映射的类型定义中选择所有属性:

type Mockify<T> = {
    [P in keyof T]?: jest.Mock<{}>
};

这相当于每次使用Partial时都使用Mockify

然后,当稍后使用我的模拟对象时,键入断言会将其强制转换回原始界面并且每个人都很高兴

const mockFoo: Mockify<IFoo> = {
    otherFunc: {
        // Mocked function behavior
    }
} 

dependentFunc(mockFoo as IFoo);

答案 1 :(得分:0)

如果我理解正确,则您正在尝试部分模拟类型。解决方法如下:

版本:

"jest": "^24.8.0",
"ts-jest": "^24.0.2",
"@types/jest": "^24.0.17",
"typescript": "^3.5.3"

Foo.ts

interface IFoo {
  myFunc(): string;
  otherFunc(): number;
}

class Foo implements IFoo {
  public myFunc(): string {
    return 'myFunc';
  }
  public otherFunc(): number {
    return 1;
  }
}

export { Foo, IFoo };

Foo中的SomeClass类用作依赖项:

import { IFoo } from './Foo';

interface ISomeClass {
  say(): string;
}

interface ISomeClassOptions {
  foo: IFoo;
}

class SomeClass implements ISomeClass {
  private foo: IFoo;
  constructor(options: ISomeClassOptions) {
    this.foo = options.foo;
  }
  public say(): string {
    return this.foo.myFunc();
  }
}

export { SomeClass, ISomeClassOptions };

单元测试中,我们只能使用foo帮助函数使用myFunc方法来部分模拟mock。它将处理打字稿的类型问题。

import { SomeClass, ISomeClassOptions } from './SomeClass';
import { mock } from '../../__utils';
import { IFoo } from './Foo';

const mockDeps: jest.Mocked<ISomeClassOptions> = {
  // foo: {
  //   myFunc: jest.fn()
  // }
  foo: mock<IFoo>('myFunc')
};

const someClass = new SomeClass(mockDeps);

describe('SomeClass', () => {
  it('#say', () => {
    (mockDeps.foo as jest.Mocked<IFoo>).myFunc.mockReturnValueOnce('https://github.com/mrdulin');
    const actualValue = someClass.say();
    expect(actualValue).toBe('https://github.com/mrdulin');
  });
});

__utils.ts

type GenericFunction = (...args: any[]) => any;

type PickByTypeKeyFilter<T, C> = {
  [K in keyof T]: T[K] extends C ? K : never;
};

type KeysByType<T, C> = PickByTypeKeyFilter<T, C>[keyof T];

type ValuesByType<T, C> = {
  [K in keyof T]: T[K] extends C ? T[K] : never;
};

type PickByType<T, C> = Pick<ValuesByType<T, C>, KeysByType<T, C>>;

type MethodsOf<T> = KeysByType<Required<T>, GenericFunction>;

type InterfaceOf<T> = PickByType<T, GenericFunction>;

type PartiallyMockedInterfaceOf<T> = {
  [K in MethodsOf<T>]?: jest.Mock<InterfaceOf<T>[K]>;
};

export function mock<T>(...mockedMethods: Array<MethodsOf<T>>): jest.Mocked<T> {
  const partiallyMocked: PartiallyMockedInterfaceOf<T> = {};
  mockedMethods.forEach(mockedMethod => (partiallyMocked[mockedMethod] = jest.fn()));
  return partiallyMocked as jest.Mocked<T>;
}

单元测试结果:

 PASS  src/stackoverflow/50217960/index.spec.ts
  SomeClass
    ✓ #say (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.207

您可以在GitHub上找到相关问题:https://github.com/facebook/jest/issues/7832#issuecomment-527449428