Ainul Ξ Creative Dev

Ainul Ξ Creative Dev

Product-focused software engineer.
twitter
tg_channel

與導入開玩笑:安全還是抱歉?

所以,我在我的工作區檢查測試時,偶然發現了這個奇怪的「require」模式,讓我感到困惑。

function getMocksForTest(): typeof import('./module-to-test') {
  // 我們不能在模組的頂層簡單地導入這個,因為必要的 jest 模擬尚未應用。
  return require('./module-to-test');
}

it('通過簡單測試', () => {
  const { functionToTest } = getMocksForTest();
  expect(functionToTest()).toEqual('foo');
});

在使用 Jest 時,我從未需要切換到 require 語句,所以看到這個我感到驚訝。通常我會直接導入模組。

import { functionToTest } from './module-to-test';

it('通過簡單測試', () => {
  expect(functionToTest()).toEqual('foo');
});

使用導入語句要簡潔得多。那麼為什麼使用了不同的模式呢?我在第一次加入時就讓它保持不變,但現在我已經進行了一些研究和軟體考古,以更好地理解這個模式的來源、何時需要它以及何時可以刪除它。

這個模式僅在使用 jest.mock 時有用#

首先讓我感到困惑的是名稱 getMocksForTest。沒有涉及模擬,所以這個名稱似乎不合適。但我後來發現了一些設置更複雜的測試文件。

function getMocksForTest() {
  const mockCreatePage = jest.fn();

  jest.mock('../pages', () => ({
    ...jest.requireActual('../pages'),
    createPage: mockCreatePage,
  }));

  // 我們不能在模組的頂層簡單地導入這個,因為必要的 jest 模擬尚未應用。
  const { functionToTest } =
    require('./module-to-test') as typeof import('./module-to-test');

  return {
    functionToTest,
    mockCreatePage,
  };
}

it('通過複雜測試', () => {
  const { functionToTest, mockCreatePage } = getMocksForTest();
  mockCreatePage.mockReturnValue('dependency');
  expect(functionToTest()).toEqual('foo+dependency');
});

在這裡,我們使用 jest.mock() 來模擬文件 ../pages.ts 中的模組。我們正在測試的模組 ./module-to-test.ts 依賴於 ../pages.ts 並導入它。

如果我們在運行 jest.mock() 之前導入了要測試的模組,那麼我們的模擬將無法及時設置。相反,將導入 ../pages.ts 的原始版本。

import { functionToTest } from './module-to-test';

function getMocksForTest() {
  const mockCreatePage = jest.fn();

  // 太遲了!`../pages` 在這個時候已經被導入了。
  jest.mock('../pages', () => ({
    ...jest.requireActual('../pages'),
    createPage: mockCreatePage,
  }));

  return {
    mockCreatePage,
  };
}

現在我遇到了一個實際創建模擬的函數,這個名稱更有意義了。後來,開發人員會複製粘貼測試並僅刪除不需要的部分。這就是為什麼我們最終會得到名為「獲取模擬」的函數,但從未進行任何模擬的原因。

但是 如果測試文件從未使用 jest.mock(),那麼這個模式完全不必要。可以清理並直接導入模組。

jest.mock() 不總是在導入語句之後運行嗎?#

現在我們知道當 jest.mock() 在模組導入之後運行時需要這個模式。但許多測試文件確實使用 jest.mock(),而我從未見過這種 require 模式的使用。那麼 jest.mock() 通常是如何工作的呢?

事實上,Jest 在運行測試文件之前會對其進行一些特殊轉換。它不僅將 TypeScript 轉換為 JavaScript,還會將 jest.mock() 調用提升到文件的頂部,在任何導入語句之前。這是通過 babel-jest 插件 完成的,該插件默認啟用。

因此,像這樣的測試文件:

// example.test.ts
import { functionToTest } from './module-to-test';

jest.mock('../pages', () => {
  const mockCreatePage: jest.MockedFn<() => string> = jest.fn();
  return {
    ...jest.requireActual('../pages'),
    createPage: mockCreatePage,
  };
});

it('通過複雜測試', () => {
  const { functionToTest, mockCreatePage } = getMocksForTest();
  mockCreatePage.mockReturnValue('dependency');
  expect(functionToTest()).toEqual('foo+dependency');
});

將被轉換為一個 Node.js 兼容的文件,如下所示:

// compliled.test.js
// 提升到文件的頂部
jest.mock('../pages', () => {
  // TypeScript 類型已移除
  const mockCreatePage = jest.fn();
  return {
    ...jest.requireActual('../pages'),
    createPage: mockCreatePage,
  };
});

// 導入語句轉換為 CommonJS
const { functionToTest } = require('./module-to-test');

it('通過複雜測試', () => {
  const { functionToTest, mockCreatePage } = getMocksForTest();
  mockCreatePage.mockReturnValue('dependency');
  expect(functionToTest()).toEqual('foo+dependency');
});

jest.mock 是否總是被提升?#

不!有特定的條件需要滿足,模組模擬才能被提升。我從挖掘 Jest 的 babel 插件的源代碼 中找到了這些檢查。

  • jest.mock() 的第一個參數應該是字面值。
  • 如果提供,第二個參數應該是內聯函數。
  • 該函數不應引用函數外部定義的任何變數。
  • jest.mock() 必須在頂層調用,而不是在另一個函數內部。
    • 否則,它將被移動到調用它的函數的頂部。

基本上,如果你不能簡單地將 jest.mock() 代碼剪切並粘貼到文件的頂部,它將不會自動移動。引用之前定義的其他變數將阻止函數被提升。

// 這些都不會被提升
const moduleName = './module';
jest.mock(moduleName);

function factory() {
  return jest.fn();
}
jest.mock('./module', factory);

jest.mock('./module', () => {
  // 在範圍外定義的變數
  return jest.fn().mockReturnValue(moduleName);
});

如果你確實需要引用在模擬函數外部定義的變數,你可以單獨導入它們或使用 jest.doMock 來明確避免提升。

import { createPage } from '../pages';

jest.mock('../pages', () => ({
  createPage: jest.fn(),
}));

const mockCreatePage = createPage as jest.Mocked<typeof createPage>;

只要你的所有模組模擬都可以被提升,使用導入語句導入要測試的代碼是安全的。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。