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.