Typescript - Mock interface with mapped types

烂漫一生 提交于 2019-12-24 03:34:51

问题


For testing purpose, I'd like to be able to create an object implementing an interface, only with function I need for my test, without having to manually maintain a mock object with all possible properties. Generally, I'm using one function at a time, so don't need to define all others but I don't want TS to keep complaining about missing properties.

For example, I have an interface IFoo :

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

I tried to create a mapped type, which assign jest.Mock<{}> to all properties of IFoo

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

Calling it like this:

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

The problem with this approach is that TS complains about the missing myFunc property on the object passed to Mockify.

I also tried Mockify<Partial<IFoo>> to ignore missing properties, but then my type defintion is different from IFoo and some other functions depending of IFoo are complaining. I could also define all properties as optionnal, but conceptually I don't like that.

I have the feeling that mapped types could make the job, but I maybe don't have the right approach.

Thanks for your help!


回答1:


I can make all properties optionnal in my mapped type definition:

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

This is equivalent to use Partial everytime I use Mockify.

Then, when using my mockified object later on, type assertion casts it back to the original interface and everybody's happy

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

dependentFunc(mockFoo as IFoo);



回答2:


If I understand correctly, you are trying to partial mock types. Here is the solution:

versions:

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

Use Foo class in SomeClass as dependency:

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

Unit test, we can partial mocked foo only with myFunc method use the mock helper function. It will handle the type issue of typescript.

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

Unit test result:

 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

You can find related issue on GitHub: https://github.com/facebook/jest/issues/7832#issuecomment-527449428



来源:https://stackoverflow.com/questions/50217960/typescript-mock-interface-with-mapped-types

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!