Jest.mock() в другом файле

У меня есть приложение для реагирования на машинописный текст, переработанное CRA.

Jest импортируется по всему миру.

Чтобы использовать шуточные макеты, мне нужно написать

jest.mock('module-name') ВЫШЕ describe() блока и В ТОМ же файле, например.

import { act, render, screen } from '@testing-library/react';
import { MyComponent } from './MyComponent';
import { something } from 'my-module';

// 150 of my 300 test suites contain these.
// THERE IS NO IMPLEMENTATION and automock is fine 
// BUT ONLY IF I opt-in. 
// The other 150 test suites might have an implementation;
// Or they might use REAL modules and not mocks. 
// This is why I don't want to just turn on `autoMock`.
// And I don't want to add this to `testSetup.js` because the OTHER 150 
// do not need them.
// But I want to find a way to call something like `TestUtilities.mockStandard()` for the first 150 test suites.
jest.mock('my-module');
jest.mock('my-module2');
jest.mock('my-module3');
jest.mock('my-module4');
jest.mock('my-module5');
jest.mock('my-module6');
jest.mock('my-module7');

describe('MyComponent', () => {
    afterAll(() => {
        jest.unmock('my-module');
        jest.unmock('my-module2');
        jest.unmock('my-module3');
        jest.unmock('my-module4');
        jest.unmock('my-module5');
        jest.unmock('my-module6');
        jest.unmock('my-module7');
    });

    afterEach(() => {
        jest.resetAllMocks();
        jest.useRealTimers();
    });

    test('component renders', async () => {
        (something as jest.MockedFunction<typeof something>).mockImplementation(() => 'banana');
        const { container } = render(<MyComponent></MyComponent>);
        expect(screen.queryByText('banana')).toBeVisible();
    });
});

Теперь у меня есть несколько сотен наборов тестов и тысячи тестов.

У некоторых из них есть ОБЩИЕ вещи, над которыми хочется посмеяться (например, my-module), но не ВСЕ.

Поэтому я не хочу использовать testSetup.js для издевательств над модулями во ВСЕХ тестах, но Я ХОЧУ найти способ не писать jest.mock() снова и снова.

Итак, я попытался создать модуль утилит следующим образом:

const TestUtilities = {
    mockMyModule: () => {
        jest.mock('my-module');
    },

    unmockMyModule: () => {
        jest.unmock('my-module');
    },
};

export default TestUtilities;

а затем изменил исходный тест:

import { act, render, screen } from '@testing-library/react';
import { MyComponent } from './MyComponent';
import { something } from 'my-module';
import TestUtilities from './TestUtilities';

TestUtilities.mockMyModule();

describe('MyComponent', () => {
    afterAll(() => {
        TestUtilities.unmockMyModule();
    });

    afterEach(() => {
        jest.resetAllMocks();
        jest.useRealTimers();
    });

    test('component renders', async () => {
        (something as jest.MockedFunction<typeof something>).mockImplementation(() => 'banana');
        const { container } = render(<MyComponent></MyComponent>);
        expect(screen.queryByText('banana')).toBeVisible();
    });
});

К сожалению, теперь тест ведет себя так, как будто над my-module не издеваются :(

Есть ли способ повторно использовать эти jest.mock() блоки?

jest.mock должен находиться в той же области, что и import имитируемого модуля, поэтому легко попробовать импортировать имитируемый модуль внутри вашей утилиты, а также предоставить средства доступа к импортированному модулю, чтобы его можно было использовать в тестах. Однако вызов методов из вашей утилиты мало чем отличается от использования jest.mock.
morganney 07.05.2024 05:02

Попробуйте jest.doMock()

Lin Du 07.05.2024 05:09

@LinDu хорошее предложение. к сожалению, не сработало, код по-прежнему ведет себя так, как будто модуль не подвергался издевательствам.

zaitsman 07.05.2024 15:49

@morganney Но на самом деле это не будет работать с тестируемым кодом, поскольку он не будет вызывать модуль из моей тестовой утилиты?

zaitsman 07.05.2024 15:50

@zaitsman Я не понимаю, о чем ты говоришь. Слова такие расплывчатые по сравнению с кодом 🙂. Однако в целом я не вижу реальной выгоды в вызове макета из утилиты по сравнению с вызовом прямо в тестовом примере. Вам все равно придется добавлять код в каждый тестовый пример, который вы хотите имитировать. Можете ли вы предоставить ссылку на свой проект на GitHub? Я мог бы посмотреть, если да.

morganney 07.05.2024 15:53

@morganney Большинство из этих сотен тестов имитируют одни и те же 5 модулей одними и теми же 5 способами. (i18n, маршрутизатор, аутентификация и т. д.) для перехватчиков useXXX по умолчанию. Если бы я мог свернуть даже 500 строк в 100 строк в куче файлов, это заметно сэкономило бы опыт разработки.

zaitsman 07.05.2024 15:55

Поэтому используйте каталог __mocks__, чтобы предоставить макеты вручную, а затем примите их, вам все равно придется звонить jest.mock туда, где вы хотите, чтобы они были макетированы, но вам не нужно предоставлять реализацию макета снова и снова. Это если один ручной макет работает для всех тестовых случаев. Вы также можете прочитать некоторую глобальную переменную внутри ручного макета, чтобы обеспечить условное макетирование.

morganney 07.05.2024 17:54

@morganney, это на самом деле ничего не сохраняет, потому что эти реализации обычно однострочные или неуказанные (например, простые jest.mock('whatever') как automock). И нет, я не хочу, чтобы ВСЕ модули автоматически подгонялись по умолчанию, я просто хочу, чтобы зависимости, общие для моих корневых/страничных компонентов, не приходилось jest.mock(); повторять снова и снова. __mocks__ полезен только тогда, когда есть возможность повторного использования.

zaitsman 08.05.2024 14:22

Опять же, вам следует расширить код примера, чтобы предоставить более подробную информацию, потому что, если вам требуются контекстные макеты, зависящие от тестового примера, я не понимаю, как может помочь утилита, просто обертывающая jest.mock. Раньше вы говорили Most of those hundreds of tests mock the same 5 modules the same 5 ways., теперь похоже, говорите обратное. Дважды проверьте пример ручных макетов в документации.

morganney 08.05.2024 15:13

@morganney, пожалуйста, см. редактирование. Честно говоря, у меня есть то, что я считаю тривиальным требованием.

zaitsman 09.05.2024 02:21

«Все или ничего» — это легко, но ваше требование «что-то так, что-то иначе» делает задачу нетривиальной. Я все еще думаю, что вам следует попробовать использовать ручной макет в каталоге __mocks__ рядом с модулем-n, а затем для 150 тестов, которые требуют специальных реализаций макетирования, используйте глобальный набор var в тестовом примере и прочитайте ручной макет, чтобы изменить имитируемый модуль. . Для остальных 150, которые могут использовать автоматическое имитационное поведение, просто не устанавливайте никаких глобальных переменных в тестах.

morganney 11.05.2024 22:44
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
11
148
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Кажется, у вас есть половина тестовых случаев, которые подходят для макетной реализации по умолчанию, а другая половина требует специальных реализаций.

Если это так, вы можете попробовать что-то вроде следующего.

Предположим, что структура каталогов такая:

.
└── src/
    └── modules/
        ├── dependency-n.js
        ├── __mocks__/
        │   └── dependency-n.js
        ├── dependent.js
        └── dependent.test.js

Ручные макеты с глобальными переменными

Вы можете использовать глобальные переменные, определенные в тестах, чтобы изменить поведение макетной реализации ручных макетов.

макеты/зависимость-n.js

let mocked = jest.fn();

if (globals.useAltMockForDepN) {
  mocked = jest.fn(() => {
    // Your custom mocking implementation based on calling test
  })
}

export default mocked;

зависимый.test.js

import { something } from 'dependency-n';

jest.mock('dependency-n');

describe('MyDependentComponent', () => {
    afterAll(() => {
        jest.unmock('dependency-module-n');
    });

    afterEach(() => {
        // Possibly use in afterAll instead
        globals.useAltMockForDepN = false
    });

    test('component renders', async () => {
        // Possibly use in beforeAll
        globals.useAltMockForDepN = true;

        // Now do your testing
    });
});

На самом деле это избавляет вас от необходимости создавать собственные реализации макета только в половине ваших тестов, и вам все равно придется вызывать jest.mock() во всех ваших тестах, если только вы не хотите/не можете имитировать каждый импортированный модуль, тогда вы можете установить automock: true. Если вы хотите использовать automock: true, вам не нужно будет вызывать jest.mock() в ваших тестах, но тогда вам придется вызывать jest.unmock() для всех импортированных модулей, которые вы не хотите имитировать.

У меня есть то, что я считаю тривиальным требованием.

Ваш сценарий не является сценарием «все или ничего», поэтому вам нужно написать код, чтобы различать ваши особые случаи, к сожалению, обойти это невозможно.

Тестовые утилиты

Вызов jest.mock() и import должны находиться в одной области, и в настоящее время вы не импортируете имитируемый модуль в свои тестовые утилиты.

Вы можете попробовать внести эти изменения.

import myModule from 'my-module'

const TestUtilities = {
    mockMyModule: () => {
        jest.mock('my-module');
    },

    unmockMyModule: () => {
        jest.unmock('my-module');
    },
   
    getMyMockedModule: () => myModule,

};

export default TestUtilities;

На самом деле вы не предоставили воспроизводимый пример, но я хотел поместить свои комментарии в код, чтобы попытаться помочь.

morganney 12.05.2024 16:53

Пожалуйста, ознакомьтесь с моим ответом: stackoverflow.com/a/78469514/2057955 Мне не очень понравилась идея глобальных переменных, так как я не уверен, как она будет работать, когда наборы тестов выполняются параллельно, к тому же это все еще было гораздо больше инструкций, чем просто рассказывать всем разработчикам add this to your spec if you want all our common stuff mocked

zaitsman 13.05.2024 01:08

Интересное решение. Обычно я не думаю о решениях, связанных с eval, но, поскольку это тестовая среда, работающая на CI-сервере где-то, недоступная для конечных пользователей, маловероятно, чтобы вредоносный код каким-либо образом проник в нее. Что касается глобальных переменных, пока ваши тесты выполняют правильную очистку, все должно быть в порядке, поскольку параллельные тесты выполняются в отдельных процессах. Рад, что вы нашли что-то, что сработало для вас и вашей команды.

morganney 13.05.2024 03:22
Ответ принят как подходящий

Итак, в конце концов, ответ кроется в злых глубинах JavaScript eval.

В частности, нам нужно создать модуль машинописного текста, который будет запускать jest.mock() как eval ВО ВРЕМЯ его загрузки.

Тогда jest runner поймет ложные вызовы в правильном контексте.

Для моего примера OP общий модуль выглядит так:

let mockCall = `(() => {
    jest.mock('my-module');
    jest.mock('my-module2');
    jest.mock('my-module4');
    jest.mock('my-module3');
    jest.mock('my-module5');
    jest.mock('my-module7');
    jest.mock('my-module6');
})()`;
eval(mockCall);

export const CommonMocksModule = {
    unmock() {
        jest.unmock('my-module');
        jest.unmock('my-module2');
        jest.unmock('my-module3');
        jest.unmock('my-module4');
        jest.unmock('my-module5');
        jest.unmock('my-module6');
        jest.unmock('my-module7');
    }
}

И спецификация становится:

// So I put this first, but really it should just go before the
// first time any of `my-moduleX` are imported 
// otherwise that import will be set and `jest.mock()` won't overwrite it.
// IMPORTANT!! we need to reference `unmock` call 
// otherwise import is not run by jest
import { CommonMocksModule } from "./_commonMocks";

import { act, render, screen } from '@testing-library/react';
import { MyComponent } from './MyComponent';
import { something } from 'my-module';

describe('MyComponent', () => {
    afterAll(() => {
        CommonMocksModule.unmock();
    });

    afterEach(() => {
        jest.resetAllMocks();
        jest.useRealTimers();
    });

    test('component renders', async () => {
        (something as jest.MockedFunction<typeof something>).mockImplementation(() => 'banana');
        const { container } = render(<MyComponent></MyComponent>);
        expect(screen.queryByText('banana')).toBeVisible();
    });
});

Достигаем нашей конечной цели — экономить на всех повторениях jest.mock()

Другие вопросы по теме

Похожие вопросы