Mock zależność żartobliwa z maszynopisem


94

Podczas testowania modułu, który ma zależność w innym pliku. Kiedy przypisanie tego modułu do typu jest.Mockmaszynopisu powoduje błąd, że metoda mockReturnThisOnce(lub jakakolwiek inna metoda jest.Mock) nie istnieje w zależności, to dlatego, że została wcześniej wpisana. Jaki jest właściwy sposób uzyskania maszynopisu, aby odziedziczył typy z jest.Mock?

Oto szybki przykład.

Zależność

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

Wydaje mi się, że jest to bardzo częsty przypadek użycia i nie jestem pewien, jak to poprawnie wpisać. Każda pomoc będzie mile widziana!


1
Jeśli dobrze pamiętam, przed importem musisz kpić. Po prostu przełącz pierwsze 2 wiersze. Ale nie jestem tego pewien.
— Thomas

3
@ ThomasKleßen Moduły importowane przez ES6 importsą oceniane jako pierwsze, bez względu na to, czy przed importem umieścisz jakiś kod. Więc to nie zadziała.
— mgol

@Thomas Wywołania jest.mock są podnoszone na początek kodu - chyba magia ... ( ref ) Jednak stwarza to pewne pułapki, np. Podczas wywoływania jest.mock () z parametrem fabrycznym modułu, dlatego nazwij funkcje mock asmock...
— Tobi

Odpowiedzi:


98

Możesz użyć rzutowania typów i Twój test.tspowinien wyglądać tak:

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

Transpiler TS nie jest świadomy, że jest.mock('../dependency');zmienia typ, depwięc musisz użyć rzutowania typów. Ponieważ import depnie jest definicją typu, musisz pobrać jej typ typeof dep.default.

Oto kilka innych przydatnych wzorców, które znalazłem podczas pracy z Jest i TS

Gdy importowany element jest klasą, nie musisz używać typeof na przykład:

import { SomeClass } from './SomeClass';

jest.mock('./SomeClass');

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

To rozwiązanie jest również przydatne, gdy musisz mockować niektóre natywne moduły węzła:

import { existsSync } from 'fs';

jest.mock('fs');

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

W przypadku, gdy nie chcesz używać automatycznego makiety i wolisz utworzyć ręczną

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()tworzy mockowaną instancję obiektu, która TestedClassDependencymoże być klasą, typem lub interfejsem


3
Musiałem użyć jest.fn(() =>...zamiast jest.fn<TestedClassDependency>(() =>...(właśnie usunąłem rzutowanie typów po jest.fn), ponieważ IntelliJ narzeka. W przeciwnym razie ta odpowiedź pomogła mi dzięki! Używając tego w moim pakiecie.json: "@ types / jest": "^ 24.0.3"
— A. Masson,

co jest.mock('./SomeClass');w powyższym kodzie?
— Reza

11
Hum, to już nie działa z ostatnią wersją TS i jest 24 :(
— Vincent


5
<jest.Mock<SomeClass>>SomeClassEkspresja powoduje błąd TS dla mnie: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

61

Użyj mockedpomocnika, ts-jestjak wyjaśniono tutaj

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

i jeśli

  • używasz tslint
  • ts-jest jest w zależnościach od deweloperów,

dodaj tę regułę do tslint.json:"no-implicit-dependencies": [true, "dev"]


Oto kilka innych przykładów użycia ts-jesti klas: github.com/tbinna/ts-jest-mock-examples i ten post: stackoverflow.com/questions/58639737/ ...
— Tobi

4
To znacznie lepsza odpowiedź niż ta, która uzyskała najwięcej głosów.
— fakeplasticandroid

@Tobi Test w repozytorium kończy się niepowodzeniem
— Kreator

Dzięki za ostrzeżenie @Kreator. Czy widzisz ten sam problem, co zgłoszony ? Nie udało mi się jeszcze odtworzyć żadnego problemu.
— Tobi

@Kreator właśnie połączył PR. Daj mi znać, jeśli problem będzie się powtarzał
— Tobi

18

Używam wzorca z @ types / jest / index.d.ts tuż nad typem def dla Mocked (wiersz 515):

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

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

2
Jestem prawie pewien, że możesz po prostu zrobićconst myApi = new Api() as jest.Mocked<Api>;
— snowfrogdev

4
@neoflash: Nie w trybie ścisłym w TypeScript 3.4 - będzie narzekać, że typ Api nie pokrywa się wystarczająco z jest.Mock<Api>. Musiałbyś iść z tym const myApi = new Api() as any as jest.Mock<Api>i powiedziałbym, że ten powyżej wygląda trochę lepiej niż podwójna asercja.
— paolostyle

@tuptus: czy tryb ścisły jest nowy w wersji 3.4? Czy masz link dotyczący tego?
— elmpp

@elmpp: nie wiem, co masz na myśli. Przez „tryb ścisły” miałem na myśli posiadanie "strict": truepliku tsconfig.json. Obejmuje to rzeczy jak noImplicitAny, strictNullChecksitd., Więc nie trzeba, aby ustawić go na true dla nich indywidualnie.
— paolostyle

Nie rozumiem. Dlaczego odrzucasz metodę tylko jednej instancji, tj. myApi? Generalnie nie będzie on zastępował wszystkich innych instancji zainicjowanych przez klasę Apiw testowanym module, prawda?
— Ivan Wang

14

Istnieją dwa rozwiązania, oba odlewają pożądaną funkcję

1) Użyj jest.MockedFunction

import * as dep from './dependency';

jest.mock('./dependency');

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

2) Użyj jest.Mock

import * as dep from './dependency';

jest.mock('./dependency');

const mockMyFunction = dep.default as jest.Mock;

Nie ma różnicy między tymi dwoma rozwiązaniami. Drugi jest krótszy, dlatego sugerowałbym użycie tego.

Oba rozwiązania castingowe pozwalają na wywołanie dowolnej funkcji jest mock na mockMyFunctionlike mockReturnValuelub mockResolvedValue https://jestjs.io/docs/en/mock-function-api.html

mockMyFunction.mockReturnValue('value');

mockMyFunction może być używany normalnie do oczekiwania

expect(mockMyFunction).toHaveBeenCalledTimes(1);

7

Odlew as jest.Mock

Proste rzutowanie funkcji na jest.Mockpowinno załatwić sprawę:

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


6

Oto, co zrobiłem z jest@24.8.0 i ts-jest@24.0.2 :

źródło:

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

Oto jak mockować klasę inną niż domyślna i jej metody statyczne:

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

Tutaj powinna być konwersja typu z typu twojej klasy na jest.MockedClasslub coś podobnego. Ale zawsze kończy się to błędami. Więc użyłem go bezpośrednio i zadziałało.

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

Ale jeśli jest to funkcja, możesz z niej kpić i przeprowadzić konwersację typu.

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

Znalazłem to w @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);
*/

Uwaga: kiedy to robisz, const mockMyFunction = myFunctiona potem coś w stylu mockFunction.mockReturnValue('foo'), również się zmieniasz myFunction.

Źródło: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/jest/index.d.ts#L1089


0

Niedawna biblioteka rozwiązuje ten problem za pomocą wtyczki babel: https://github.com/userlike/joke

Przykład:

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

Pamiętaj o tym depi mockReturnValueOncesą w pełni bezpieczne. Poza tym tsserver jest świadomy tego, że depencencyzostał zaimportowany i został do niego przypisany, depwięc wszystkie automatyczne refaktoryzacje obsługiwane przez tsserver również będą działać.

Uwaga: prowadzę bibliotekę.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.