Знущання над залежністю в жарті з машинописом


94

Під час тестування модуля, який має залежність в іншому файлі. Коли присвоєння цього модуля jest.Mockтипограмою видає помилку, що метод mockReturnThisOnce(або будь-який інший метод jest.Mock) не існує на залежності, це тому, що він раніше вводився. Який правильний спосіб отримати typecript для успадкування типів з jest.Mock?

Ось короткий приклад.

Залежність

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

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

Я відчуваю, що це дуже поширений випадок використання, і я не знаю, як правильно це вводити. Будь-яка допомога буде дуже вдячна!


1
Якщо я правильно пам’ятаю, перед імпортом вам потрібно знущатися. Просто переключіть перші 2 рядки. Але я не впевнений у цьому.
Томас

3
Модулі @ ThomasKleßen, імпортовані за допомогою ES6 import, оцінюються спочатку, незалежно від того, якщо перед імпортом ви вставите якийсь код. Тож це не спрацює.
mgol

@Thomas Виклики до jest.mock піднімаються до верхньої частини коду - jest magic, я думаю ... ( ref ) Однак це створює деякі підводні камені, наприклад, під час виклику jest.mock () із параметром заводу модуля, тому називають макетні функції якmock...
Тобі

Відповіді:


98

Ви можете використовувати кастинг типу, і ви 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;

Якщо ви не хочете використовувати автоматичний макет і віддаєте перевагу створити його вручну

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може бути або класом, або типом, або інтерфейсом


3
Мені довелося використовувати jest.fn(() =>...замість jest.fn<TestedClassDependency>(() =>...(я щойно видалив кастинг типу після jest.fn), оскільки IntelliJ скаржиться. Інакше ця відповідь мені допомогла! Використовуючи це в моєму пакеті.json: "@ types / jest": "^ 24.0.3"
А. Массон,

що робиться jest.mock('./SomeClass');у наведеному вище коді?
Реза

11
Хам, це вже не працює з останньою версією TS і жартом 24 :(
Вінсент,


6
<jest.Mock<SomeClass>>SomeClassВираз викликає помилку TS для мене:Conversion of type 'typeof SomeClass' to type 'Mock<SomeClass, any>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Type 'typeof SomeClass' is missing the following properties from type 'Mock<SomeClass, any>': getMockName, mock, mockClear, mockReset, and 11 more.ts(2352)
the21st

62

Скористайтеся mockedпомічником із ts-jestописаного тут

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

// here the whole foo var is mocked deeply
const mockedFoo = 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(mocked(foo.name).mock.calls).toHaveLength(1)
})

і якщо

  • ти використовуєш tslint
  • ts-jest знаходиться у ваших залежностях від розробника,

додайте це правило до свого tslint.json:"no-implicit-dependencies": [true, "dev"]


Ось ще кілька прикладів використання ts-jestта класів: github.com/tbinna/ts-jest-mock-examples та цей допис: stackoverflow.com/questions/58639737/…
Тобі

5
Це набагато краща відповідь, ніж відповідь з найбільшою оцінкою.
fakeplasticandroid

@Tobi Тест в репо не вдається
Kreator

Дякуємо за огляд @Kreator. Ви бачите ту саму проблему, що і та, про яку повідомляли ? Я ще не міг відтворити жодного випуску.
Тобі

@Kreator щойно об’єднав PR. Повідомте мене, якщо проблема не зникне
Тобі,

18

Я використовую шаблон із @ types / jest / index.d.ts трохи вище типу def для Mocked (рядок 515):

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

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

2
Я майже впевнений, що ви могли це зробитиconst myApi = new Api() as jest.Mocked<Api>;
snowfrogdev

4
@neoflash: Не в суворому режимі в TypeScript 3.4 - він скаржиться, що тип Api недостатньо перекривається jest.Mock<Api>. Вам потрібно було б піти, const myApi = new Api() as any as jest.Mock<Api>і я б сказав, що наведений вище виглядає дещо краще, ніж подвійне твердження.
paolostyle

@tuptus: чи строгий режим свіжий для 3.4? У вас є посилання щодо цього?
elmpp

@elmpp: не знаю, що ти маєш на увазі. Під "суворим режимом" я мав на увазі наявність "strict": trueу tsconfig.json. Це охоплює такі речі , як noImplicitAny, і strictNullChecksт.д., так що ви не повинні встановити його правильно і для них в окремо.
паолостиль

Я не розумію. Чому ви заглушаєте метод лише одного екземпляра, тобто myApi? Це загалом не буде заглушати всі інші екземпляри, ініційовані класом Apiу модулі, що тестується, чи не так?
Іван Ван

14

Існує два рішення, обидва виконують необхідну функцію

1) Використовуйте жарт. MockedFunction

import * as dep from './dependency';

jest.mock('./dependency');

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

2) Використовуйте жарт

import * as dep from './dependency';

jest.mock('./dependency');

const mockMyFunction = dep.default as jest.Mock;

Між цими двома рішеннями немає різниці. Другий коротший, і тому я б запропонував використовувати його.

Обидва рішення кастингу дозволяють викликати будь-яку функцію жарту на mockMyFunctionlike mockReturnValueабо mockResolvedValue https://jestjs.io/docs/en/mock-function-api.html

mockMyFunction.mockReturnValue('value');

mockMyFunction можна використовувати звичайно для очікування

expect(mockMyFunction).toHaveBeenCalledTimes(1);

7

Актори as jest.Mock

Просто кинувши функцію, jest.Mockслід зробити трюк:

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


6

Ось що я зробив з jest@24.8.0 і ts-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
>;

4

Я знайшов це в @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


1

Використання as jest.Mockі нічого іншого

Найкоротший спосіб знущання над модулем, експортованим, як defaultу ts-jest, який я пригадую, справді зводиться до викидання модуля як 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в заяві про імпорт,
  • відсутність складного кастингу за typeofключовим словом,
  • ніяких зайвих залежностей типу mocked.

0

Нещодавня бібліотека вирішує цю проблему за допомогою плагіна 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');
}

Майте на увазі, що depі mockReturnValueOnceє повністю безпечними. Крім того, tsserver знає, що depencencyбуло імпортовано та призначено йому, depтому всі автоматичні рефакторинг, які підтримує tsserver, також працюватимуть.

Примітка: Я підтримую бібліотеку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.