简体   繁体   English

使用 TypeScript 在 Jest 中模拟依赖

[英]Mock dependency in Jest with TypeScript

When testing a module that has a dependency in a different file and assigning that module to be a jest.mock , TypeScript gives an error that the method mockReturnThisOnce (or any other jest.mock method) does not exist on the dependency, this is because it is previously typed.当测试在不同文件中具有依赖关系的模块并将该模块分配为jest.mock时,TypeScript 会给出一个错误,即方法mockReturnThisOnce (或任何其他jest.mock方法)在依赖项中不存在,这是因为它是以前键入的。

What is the proper way to get TypeScript to inherit the types from jest.mock ?让 TypeScript 从jest.mock继承类型的正确方法是什么?

Here is a quick example.这是一个简单的例子。

Dependency依赖

const myDep = (name: string) => name;
export default myDep;

test.ts测试.ts

import * as dep from '../depenendency';
jest.mock('../dependency');

it('should do what I need', () => {
  //this throws ts error
  // Property mockReturnValueOnce does not exist on type (name: string)....
  dep.default.mockReturnValueOnce('return')
}

I feel like this is a very common use case and not sure how to properly type this.我觉得这是一个非常常见的用例,不知道如何正确输入。

You can use type casting and your test.ts should look like this:您可以使用类型转换,您的test.ts应该如下所示:

import * as dep from '../dependency';
jest.mock('../dependency');

const mockedDependency = <jest.Mock<typeof dep.default>>dep.default;

it('should do what I need', () => {
  //this throws ts error
  // Property mockReturnValueOnce does not exist on type (name: string)....
  mockedDependency.mockReturnValueOnce('return');
});

TS transpiler is not aware that jest.mock('../dependency'); TS 编译器不知道jest.mock('../dependency'); changes type of dep thus you have to use type casting.更改dep的类型,因此您必须使用类型转换。 As imported dep is not a type definition you have to get its type with typeof dep.default .由于导入的dep不是类型定义,因此您必须使用typeof dep.default获取其类型。

Here are some other useful patterns I've found during my work with Jest and TS以下是我在使用 Jest 和 TS 的过程中发现的其他一些有用的模式

When imported element is a class then you don't have to use typeof for example:当导入的元素是一个类时,您不必使用 typeof 例如:

import { SomeClass } from './SomeClass';

jest.mock('./SomeClass');

const mockedClass = <jest.Mock<SomeClass>>SomeClass;

This solution is also useful when you have to mock some node native modules:当您必须模拟一些节点本机模块时,此解决方案也很有用:

import { existsSync } from 'fs';

jest.mock('fs');

const mockedExistsSync = <jest.Mock<typeof existsSync>>existsSync;

In case you don't want to use jest automatic mock and prefer create manual one如果您不想使用 jest 自动模拟并更喜欢创建手动模拟

import TestedClass from './TestedClass';
import TestedClassDependency from './TestedClassDependency';

const testedClassDependencyMock = jest.fn<TestedClassDependency>(() => ({
  // implementation
}));

it('Should throw an error when calling playSomethingCool', () => {
  const testedClass = new TestedClass(testedClassDependencyMock());
});

testedClassDependencyMock() creates mocked object instance TestedClassDependency can be either class or type or interface testedClassDependencyMock()创建模拟对象实例TestedClassDependency可以是类或类型或接口

Use the mocked helper like explained here使用此处解释mocked助手

// foo.spec.ts
import { foo } from './foo'
jest.mock('./foo')

// here the whole foo var is mocked deeply
const mockedFoo = jest.mocked(foo, true)

test('deep', () => {
  // there will be no TS error here, and you'll have completion in modern IDEs
  mockedFoo.a.b.c.hello('me')
  // same here
  expect(mockedFoo.a.b.c.hello.mock.calls).toHaveLength(1)
})

test('direct', () => {
  foo.name()
  // here only foo.name is mocked (or its methods if it's an object)
  expect(jest.mocked(foo.name).mock.calls).toHaveLength(1)
})

There are two solutions tested for TypeScript version 3.x and 4.x , both are casting desired function针对TypeScript 版本 3.x 和 4.x测试了两种解决方案,两者都在转换所需的功能

1) Use jest.MockedFunction 1) 使用 jest.MockedFunction

import * as dep from './dependency';

jest.mock('./dependency');

const mockMyFunction = dep.myFunction as jest.MockedFunction<typeof dep.myFunction>;

2) Use jest.Mock 2) 使用 jest.Mock

import * as dep from './dependency';

jest.mock('./dependency');

const mockMyFunction = dep.default as jest.Mock;

There is no difference between these two solutions.这两种解决方案之间没有区别。 The second one is shorter and I would therefore suggest using that one.第二个更短,因此我建议使用那个。

Both casting solutions allows to call any jest mock function on mockMyFunction like mockReturnValue or mockResolvedValue https://jestjs.io/docs/en/mock-function-api.html两种铸造解决方案都允许在mockMyFunction上调用任何 jest 模拟函数,例如mockReturnValuemockResolvedValue https://jestjs.io/docs/en/mock-function-api.html

mockMyFunction.mockReturnValue('value');

mockMyFunction can be used normally for expect mockMyFunction可以正常用于 expect

expect(mockMyFunction).toHaveBeenCalledTimes(1);

I use the pattern from @types/jest/index.d.ts just above the type def for Mocked (line 515):我使用来自 @types/jest/index.d.ts 的模式,就在 Mocked 的类型 def 上方(第 515 行):

import { Api } from "../api";
jest.mock("../api");

const myApi: jest.Mocked<Api> = new Api() as any;
myApi.myApiMethod.mockImplementation(() => "test");

Cast as jest.Mock as jest.Mock

Simply casting the function to jest.Mock should do the trick:只需将函数转换为jest.Mock就可以了:

(dep.default as jest.Mock).mockReturnValueOnce('return')

Use as jest.Mock and nothing else as jest.Mock ,仅此而已

The most concise way of mocking a module exported as default in ts-jest that I can think of really boils down to casting the module as jest.Mock .我能想到的在 ts-jest 中模拟default导出的模块的最简洁方法实际上归结为将模块转换为jest.Mock

Code:代码:

import myDep from '../dependency' // No `* as` here

jest.mock('../dependency')

it('does what I need', () => {
  // Only diff with pure JavaScript is the presence of `as jest.Mock`
  (myDep as jest.Mock).mockReturnValueOnce('return')

  // Call function that calls the mocked module here

  // Notice there's no reference to `.default` below
  expect(myDep).toHaveBeenCalled()
})

Benefits:好处:

  • does not require referring to the default property anywhere in the test code - you reference the actual exported function name instead,不需要在测试代码中的任何地方引用default属性 - 您可以引用实际导出的函数名称,
  • you can use the same technique for mocking named exports,您可以使用相同的技术来模拟命名导出,
  • no * as in the import statement,没有* as在 import 语句中,
  • no complex casting using the typeof keyword,无需使用typeof关键字进行复杂的转换,
  • no extra dependencies like mocked .没有像mocked这样的额外依赖项。

Here's what I did with jest@24.8.0 and ts-jest@24.0.2 :这是我对jest@24.8.0ts-jest@24.0.2 所做的:

source:资源:

class OAuth {

  static isLogIn() {
    // return true/false;
  }

  static getOAuthService() {
    // ...
  }
}

test:测试:

import { OAuth } from '../src/to/the/OAuth'

jest.mock('../src/utils/OAuth', () => ({
  OAuth: class {
    public static getOAuthService() {
      return {
        getAuthorizationUrl() {
          return '';
        }
      };
    }
  }
}));

describe('createMeeting', () => {
  test('should call conferenceLoginBuild when not login', () => {
    OAuth.isLogIn = jest.fn().mockImplementationOnce(() => {
      return false;
    });

    // Other tests
  });
});

This is how to mock a non-default class and it's static methods:这是模拟非默认类及其静态方法的方法:

jest.mock('../src/to/the/OAuth', () => ({
  OAuth: class {
    public static getOAuthService() {
      return {
        getAuthorizationUrl() {
          return '';
        }
      };
    }
  }
}));

Here should be some type conversion from the type of your class to jest.MockedClass or something like that.这应该是从您的类的类型到jest.MockedClass或类似的类型的某种类型转换。 But it always ends up with errors.但它总是以错误告终。 So I just used it directly, and it worked.所以我只是直接使用它,它工作。

test('Some test', () => {
  OAuth.isLogIn = jest.fn().mockImplementationOnce(() => {
    return false;
  });
});

But, if it's a function, you can mock it and do the type conversation.但是,如果它是一个函数,您可以模拟它并进行类型对话。

jest.mock('../src/to/the/Conference', () => ({
  conferenceSuccessDataBuild: jest.fn(),
  conferenceLoginBuild: jest.fn()
}));
const mockedConferenceLoginBuild = conferenceLoginBuild as 
jest.MockedFunction<
  typeof conferenceLoginBuild
>;
const mockedConferenceSuccessDataBuild = conferenceSuccessDataBuild as 
jest.MockedFunction<
  typeof conferenceSuccessDataBuild
>;

I have found this in @types/jest :我在@types/jest找到了这个:

/**
  * Wrap a function with mock definitions
  *
  * @example
  *
  *  import { myFunction } from "./library";
  *  jest.mock("./library");
  *
  *  const mockMyFunction = myFunction as jest.MockedFunction<typeof myFunction>;
  *  expect(mockMyFunction.mock.calls[0][0]).toBe(42);
*/

Note: When you do const mockMyFunction = myFunction and then something like mockFunction.mockReturnValue('foo') , you're a changing myFunction as well.注意:当您执行const mockMyFunction = myFunction然后执行类似mockFunction.mockReturnValue('foo')时,您也是一个不断变化的myFunction

Source: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/jest/index.d.ts#L1089来源: https ://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/jest/index.d.ts#L1089

As of Jest 24.9.0 here is how you can mock and correctly type both your Class/Object/function and Jest properties.从 Jest 24.9.0开始,您可以模拟和正确键入 Class/Object/function 和 Jest 属性。

jest.MockedFunction jest.MockedFunction

jest.MockedClass jest.MockedClass

What we would like for a typed mock is that the mocked object type contains the union of the mocked object type and the type of Jest mocks.对于类型化的模拟,我们想要的是模拟对象类型包含模拟对象类型和 Jest 模拟类型的联合。

import foo from 'foo';
jest.mock('foo');

const mockedFoo = foo as jest.MockedFunction<typeof foo>;
// or: const mockedFooClass = foo as jest.MockedClass<typeof FooClass>;


mockedFoo.mockResolvedValue('mockResult');

// Or:
(mockedFoo.getSomething as jest.MockedFunction<typeof mockedFoo.getSomething>).mockResolvedValue('mockResult');

As you can see, you can either manually cast what you need or you'll need something to traverse all foo 's properties/methods to type/cast everything.如您所见,您可以手动转换您需要的内容,或者您​​需要一些东西来遍历所有foo的属性/方法来键入/转换所有内容。

To do that (deep mock types) you can use jest.mocked() introduced in Jest 27.4.0为此(深度模拟类型),您可以使用 Jest 27.4.0中引入的jest.mocked()

import foo from 'foo';
jest.mock('foo');

const mockedFoo = jest.mocked(foo, true); 

mockedFoo.mockImplementation() // correctly typed
mockedFoo.getSomething.mockImplementation() // also correctly typed

The top rated solution by Artur Górski does not work with the last TS and Jest. Artur Górski 评价最高的解决方案不适用于最后一个 TS 和 Jest。 Use MockedClass使用模拟类

import SoundPlayer from '../sound-player';

jest.mock('../sound-player'); // SoundPlayer is now a mock constructor

const SoundPlayerMock = SoundPlayer as jest.MockedClass<typeof SoundPlayer>;

The latest jest allows you to do this very easily with jest.mocked最新的 jest 允许您使用jest.mocked轻松完成此操作

import * as dep from '../dependency';

jest.mock('../dependency');

const mockedDependency = jest.mocked(dep);

it('should do what I need', () => {
  mockedDependency.mockReturnValueOnce('return');
});
import api from 'api'
jest.mock('api')
const mockApi = (api as unknown) as jest.Mock

This is ugly, and in fact getting away from this ugliness is why I even looked at this question, but to get strong typing from a module mock, you can do something like this:这很丑陋,实际上摆脱这种丑陋是我什至看这个问题的原因,但是要从模块模拟中获得强类型,您可以执行以下操作:

const myDep = (require('./dependency') as import('./__mocks__/dependency')).default;

jest.mock('./dependency');

Make sure you require './dependency' rather than the mock directly, or you will get two different instantiations.确保您需要'./dependency'而不是直接模拟,否则您将获得两个不同的实例化。

For me this was enough:对我来说,这就足够了:

let itemQ: queueItemType
jest.mock('../dependency/queue', () => {
    return {
        add: async (item: queueItemType, ..._args: any) => {
            // then we can use the item that would be pushed to the queue in our tests
            itemQ = item
            return new Promise(resolve => {
                resolve('Mocked')
            })
        },
    }
})

Then, whenever the add method is called it will execute this code above instead of pushing it to the queue, in this case.然后,无论何时调用 add 方法,它都会执行上面的代码,而不是将其推送到队列中,在这种情况下。

With TypeScript 2.8 we can do like this with ReturnType :使用ReturnType 2.8 我们可以使用 ReturnType 这样做:

import * as dep from "./depenendency"

jest.mock("./dependency")

const mockedDependency = <jest.Mock<ReturnType<typeof dep.default>>>dep.default

it("should do what I need", () => {
  mockedDependency.mockReturnValueOnce("return")
})

Since we're talking about a test, a quick and dirty way is to just to tell TypeScript to ignore this line: 由于我们在谈论测试,因此一种快速而肮脏的方法是仅告诉TypeScript忽略此行:

//@ts-ignore
dep.default.mockReturnValueOnce('return')

A recent library solves this problem with a babel plugin: https://github.com/userlike/joke最近的一个库用 babel 插件解决了这个问题: https ://github.com/userlike/joke

Example:例子:

import { mock, mockSome } from 'userlike/joke';

const dep = mock(import('./dependency'));

// You can partially mock a module too, completely typesafe!
// thisIsAMock has mock related methods
// thisIsReal does not have mock related methods
const { thisIsAMock, thisIsReal } = mockSome(import('./dependency2'), () => ({ 
  thisIsAMock: jest.fn() 
}));

it('should do what I need', () => {
  dep.mockReturnValueOnce('return');
}

Be aware that dep and mockReturnValueOnce are fully type safe.请注意depmockReturnValueOnce是完全类型安全的。 On top, tsserver is aware that depencency was imported and was assigned to dep so all automatic refactorings that tsserver supports will work too.最重要的是, depencency知道依赖已导入并分配给dep ,因此 tsserver 支持的所有自动重构也将起作用。

Note: I maintain the library.注意:我维护图书馆。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM