簡體   English   中英

使用 TypeScript 在 Jest 中模擬依賴

[英]Mock dependency in Jest with TypeScript

當測試在不同文件中具有依賴關系的模塊並將該模塊分配為jest.mock時,TypeScript 會給出一個錯誤,即方法mockReturnThisOnce (或任何其他jest.mock方法)在依賴項中不存在,這是因為它是以前鍵入的。

讓 TypeScript 從jest.mock繼承類型的正確方法是什么?

這是一個簡單的例子。

依賴

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

測試.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')
}

我覺得這是一個非常常見的用例,不知道如何正確輸入。

您可以使用類型轉換,您的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 編譯器不知道jest.mock('../dependency'); 更改dep的類型,因此您必須使用類型轉換。 由於導入的dep不是類型定義,因此您必須使用typeof dep.default獲取其類型。

以下是我在使用 Jest 和 TS 的過程中發現的其他一些有用的模式

當導入的元素是一個類時,您不必使用 typeof 例如:

import { SomeClass } from './SomeClass';

jest.mock('./SomeClass');

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

當您必須模擬一些節點本機模塊時,此解決方案也很有用:

import { existsSync } from 'fs';

jest.mock('fs');

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

如果您不想使用 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()創建模擬對象實例TestedClassDependency可以是類或類型或接口

使用此處解釋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)
})

針對TypeScript 版本 3.x 和 4.x測試了兩種解決方案,兩者都在轉換所需的功能

1) 使用 jest.MockedFunction

import * as dep from './dependency';

jest.mock('./dependency');

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

2) 使用 jest.Mock

import * as dep from './dependency';

jest.mock('./dependency');

const mockMyFunction = dep.default as jest.Mock;

這兩種解決方案之間沒有區別。 第二個更短,因此我建議使用那個。

兩種鑄造解決方案都允許在mockMyFunction上調用任何 jest 模擬函數,例如mockReturnValuemockResolvedValue https://jestjs.io/docs/en/mock-function-api.html

mockMyFunction.mockReturnValue('value');

mockMyFunction可以正常用於 expect

expect(mockMyFunction).toHaveBeenCalledTimes(1);

我使用來自 @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");

as jest.Mock

只需將函數轉換為jest.Mock就可以了:

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

as jest.Mock ,僅此而已

我能想到的在 ts-jest 中模擬default導出的模塊的最簡潔方法實際上歸結為將模塊轉換為jest.Mock

代碼:

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()
})

好處:

  • 不需要在測試代碼中的任何地方引用default屬性 - 您可以引用實際導出的函數名稱,
  • 您可以使用相同的技術來模擬命名導出,
  • 沒有* as在 import 語句中,
  • 無需使用typeof關鍵字進行復雜的轉換,
  • 沒有像mocked這樣的額外依賴項。

這是我對jest@24.8.0ts-jest@24.0.2 所做的:

資源:

class OAuth {

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

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

測試:

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

這是模擬非默認類及其靜態方法的方法:

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

這應該是從您的類的類型到jest.MockedClass或類似的類型的某種類型轉換。 但它總是以錯誤告終。 所以我只是直接使用它,它工作。

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

但是,如果它是一個函數,您可以模擬它並進行類型對話。

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

我在@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);
*/

注意:當您執行const mockMyFunction = myFunction然后執行類似mockFunction.mockReturnValue('foo')時,您也是一個不斷變化的myFunction

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

從 Jest 24.9.0開始,您可以模擬和正確鍵入 Class/Object/function 和 Jest 屬性。

jest.MockedFunction

jest.MockedClass

對於類型化的模擬,我們想要的是模擬對象類型包含模擬對象類型和 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');

如您所見,您可以手動轉換您需要的內容,或者您​​需要一些東西來遍歷所有foo的屬性/方法來鍵入/轉換所有內容。

為此(深度模擬類型),您可以使用 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

Artur Górski 評價最高的解決方案不適用於最后一個 TS 和 Jest。 使用模擬類

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

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

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

最新的 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

這很丑陋,實際上擺脫這種丑陋是我什至看這個問題的原因,但是要從模塊模擬中獲得強類型,您可以執行以下操作:

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

jest.mock('./dependency');

確保您需要'./dependency'而不是直接模擬,否則您將獲得兩個不同的實例化。

對我來說,這就足夠了:

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')
            })
        },
    }
})

然后,無論何時調用 add 方法,它都會執行上面的代碼,而不是將其推送到隊列中,在這種情況下。

使用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")
})

由於我們在談論測試,因此一種快速而骯臟的方法是僅告訴TypeScript忽略此行:

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

最近的一個庫用 babel 插件解決了這個問題: https ://github.com/userlike/joke

例子:

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

請注意depmockReturnValueOnce是完全類型安全的。 最重要的是, depencency知道依賴已導入並分配給dep ,因此 tsserver 支持的所有自動重構也將起作用。

注意:我維護圖書館。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM