Jak mogę wyśmiewać import modułu ES6 za pomocą Jest?

281

Zaczynam myśleć, że to nie jest możliwe, ale i tak chcę zapytać.

Chcę przetestować, czy jeden z moich modułów ES6 wywołuje inny moduł ES6 w określony sposób. Z Jasmine jest to bardzo łatwe -

Kod aplikacji:

// myModule.js
import dependency from './dependency';

export default (x) => {
  dependency.doSomething(x * 2);
}

I kod testowy:

//myModule-test.js
import myModule from '../myModule';
import dependency from '../dependency';

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    spyOn(dependency, 'doSomething');

    myModule(2);

    expect(dependency.doSomething).toHaveBeenCalledWith(4);
  });
});

Jaki jest odpowiednik Jest? Wydaje mi się, że jest to bardzo prosta rzecz, ale chcę oderwać włosy, próbując to rozgryźć.

Najbliższe, jakie przyszedłem, to zamiana imports na requires i przenoszenie ich wewnątrz testów / funkcji. Żadnej z tych rzeczy nie chcę robić.

// myModule.js
export default (x) => {
  const dependency = require('./dependency'); // yuck
  dependency.doSomething(x * 2);
}

//myModule-test.js
describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    jest.mock('../dependency');

    myModule(2);

    const dependency = require('../dependency'); // also yuck
    expect(dependency.doSomething).toBeCalledWith(4);
  });
});

Jeśli chodzi o punkty bonusowe, chciałbym, aby wszystko działało, gdy funkcja wewnątrz dependency.jsjest domyślnym eksportem. Wiem jednak, że szpiegowanie domyślnych eksportów nie działa w Jasmine (a przynajmniej nigdy nie udało mi się go uruchomić), więc nie mam nadziei, że jest to możliwe w Jest.

Cam Jackson
źródło
W każdym razie używam Babel do tego projektu, więc nie mam nic przeciwko dalszemu transponowaniu imports do requires. Dzięki za heads-upy.
Cam Jackson,
co jeśli mam klasę ts A i wywołuje ona jakąś funkcję, powiedzmy doSomething () klasy B, w jaki sposób możemy wyśmiewać, aby klasa A wywoływała fałszywą wersję funkcji klasy
doSomething
dla tych, którzy chcą odkryć ten problem więcej github.com/facebook/jest/issues/936
omeralper

Odpowiedzi:

221

Udało mi się to rozwiązać za pomocą włamania import *. Działa nawet w przypadku nazwanych i domyślnych eksportów!

W przypadku nazwanego eksportu:

// dependency.js
export const doSomething = (y) => console.log(y)

// myModule.js
import { doSomething } from './dependency';

export default (x) => {
  doSomething(x * 2);
}

// myModule-test.js
import myModule from '../myModule';
import * as dependency from '../dependency';

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    dependency.doSomething = jest.fn(); // Mutate the named export

    myModule(2);

    expect(dependency.doSomething).toBeCalledWith(4);
  });
});

Lub w przypadku domyślnego eksportu:

// dependency.js
export default (y) => console.log(y)

// myModule.js
import dependency from './dependency'; // Note lack of curlies

export default (x) => {
  dependency(x * 2);
}

// myModule-test.js
import myModule from '../myModule';
import * as dependency from '../dependency';

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    dependency.default = jest.fn(); // Mutate the default export

    myModule(2);

    expect(dependency.default).toBeCalledWith(4); // Assert against the default
  });
});

Jak słusznie wskazał Mihai Damian poniżej, powoduje to mutację obiektu modułu dependency, a więc „przecieka” do innych testów. Jeśli więc zastosujesz to podejście, powinieneś zapisać oryginalną wartość, a następnie ustawić ją ponownie po każdym teście. Aby to zrobić z pomocą Jest, użyj metody spyOn () zamiast, jest.fn()ponieważ obsługuje łatwe przywracanie pierwotnej wartości, dzięki czemu unika się wcześniej wspomnianego „wycieku”.

Cam Jackson
źródło
Dzięki za udostępnienie. Myślę, że wynik netto jest podobny do tego - ale może być czystszy - stackoverflow.com/a/38414160/1882064
arcseldon
64
To działa, ale prawdopodobnie nie jest to dobra praktyka. Wydaje się, że zmiany w obiektach poza zakresem testu utrzymują się między testami. Może to później prowadzić do nieoczekiwanych wyników w innych testach.
Mihai Damian
10
Zamiast używać jest.fn (), możesz użyć jest.spyOn (), abyś mógł później przywrócić oryginalną metodę, aby nie spadła do innych testów. Znalazłem tutaj fajny artykuł o różnych podejściach (jest.fn, jest.mock i jest.spyOn): medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c .
Martinsos,
2
Tylko uwaga: jeśli dependencyplik znajduje się w tym samym pliku co myModule, nie będzie działać.
Lu Tran
2
Myślę, że to nie zadziała z maszynowym skryptem obiekt, który mutujesz jest tylko do odczytu.
adredx
172

Musisz wyśmiewać moduł i sam ustawić szpiega:

import myModule from '../myModule';
import dependency from '../dependency';
jest.mock('../dependency', () => ({
  doSomething: jest.fn()
}))

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    myModule(2);
    expect(dependency.doSomething).toBeCalledWith(4);
  });
});
Andreas Köberle
źródło
4
To nie wydaje się właściwe. Rozumiem: babel-plugin-jest-hoist: The second argument of jest.mock must be a function.więc kod nawet się nie kompiluje.
Cam Jackson,
3
Przepraszam, zaktualizowałem swój kod. Należy również pamiętać, że ścieżka w jest.mockjest względna do pliku testowego.
Andreas Köberle,
1
Działa to jednak dla mnie nie przy domyślnym eksporcie.
Iris Schaffer
4
@IrisSchaffer, aby mieć tę pracę z domyślnym eksportem, musisz dodać __esModule: truedo obiektu próbnego . Jest to wewnętrzna flaga używana przez transpilowany kod do ustalenia, czy jest to transpilowany moduł es6 czy moduł commonjs.
Johannes Lumpe
24
Wyśmiewanie domyślnych eksportów: jest.mock('../dependency', () => ({ default: jest.fn() }))
Neob91
50

Aby wyśmiewać domyślny eksport modułu zależności ES6 za pomocą jest:

import myModule from '../myModule';
import dependency from '../dependency';

jest.mock('../dependency');

// If necessary, you can place a mock implementation like this:
dependency.mockImplementation(() => 42);

describe('myModule', () => {
  it('calls the dependency once with double the input', () => {
    myModule(2);

    expect(dependency).toHaveBeenCalledTimes(1);
    expect(dependency).toHaveBeenCalledWith(4);
  });
});

Inne opcje nie działały w moim przypadku.

falsarella
źródło
6
jaki jest najlepszy sposób, aby to wyczyścić, jeśli chcę zrobić tylko jeden test? w środku poEach? `` `afterEach (() => {jest.unmock (../ dependence ');})` `
nxmohamad
1
@falsarella czy doMock faktycznie działa w takim przypadku? Mam bardzo podobny problem i nic nie robi, gdy próbuję być.doMock w ramach konkretnego testu, gdzie jest.mock dla całego modułu działa poprawnie
Progress1ve
1
@ Progress1ve możesz także spróbować użyć jest.mock z mockImplementationOnce
falsarella
1
Tak, to słuszna sugestia, która jednak wymaga testu jako pierwszego i nie jestem fanem pisania testów w taki sposób. Rozwiązałem te problemy, importując moduł zewnętrzny i używając spyOn do określonych funkcji.
Progress1ve
1
@ Progress1ve hmm Chciałem umieścić mockImplementationOnce w każdym konkretnym teście ... tak czy inaczej, cieszę się, że znalazłeś rozwiązanie :)
falsarella
38

Dodając więcej do odpowiedzi Andreasa. Miałem ten sam problem z kodem ES6, ale nie chciałem mutować importu. To wyglądało okropnie. Więc to zrobiłem

import myModule from '../myModule';
import dependency from '../dependency';
jest.mock('../dependency');

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    myModule(2);
  });
});

I dodano plik dependence.js w folderze „__ mocks __” równolegle do pliku dependency.js. To zadziałało dla mnie. Dało mi to również możliwość zwrócenia odpowiednich danych z fałszywej implementacji. Upewnij się, że podałeś prawidłową ścieżkę do modułu, z którego chcesz się wyśmiewać.

mdsAyubi
źródło
Dzięki za to. Spróbuje. Podobało mi się także to rozwiązanie - stackoverflow.com/a/38414160/1882064
arcseldon
To, co podoba mi się w tym podejściu, polega na tym, że daje ono możliwość zapewnienia jednej ręcznej makiety na wszystkie okazje, w których chcesz kpić z określonego modułu. Mam na przykład pomocnika tłumacza, który jest używany w wielu miejscach. __mocks__/translations.jsPlik eksportu domyślnie po prostu jest.fn()w coś takiego:export default jest.fn((id) => id)
Iris Schaffer
Możesz także użyć jest.genMockFromModuledo generowania próbnych modułów z modułów. facebook.github.io/jest/docs/…
Varunkumar Nagarajan
2
Należy zauważyć, że wyśmiewane przez moduły ES6 export default jest.genMockFromModule('../dependency')będą miały przypisane wszystkie swoje funkcje dependency.defaultpo wywołaniu `jest.mock ('.. dependence'), ale poza tym zachowują się zgodnie z oczekiwaniami.
jhk
7
Jak wygląda twoje twierdzenie testowe? To wydaje się być ważną częścią odpowiedzi. expect(???)
kamień
14

Szybkie przejście do 2020 roku, znalazłem ten link jako rozwiązanie. za pomocą tylko składni modułu ES6 https://remarkablemark.org/blog/2018/06/28/jest-mock-default-named-export/

// esModule.js
export default 'defaultExport';
export const namedExport = () => {};

// esModule.test.js
jest.mock('./esModule', () => ({
  __esModule: true, // this property makes it work
  default: 'mockedDefaultExport',
  namedExport: jest.fn(),
}));

import defaultExport, { namedExport } from './esModule';
defaultExport; // 'mockedDefaultExport'
namedExport; // mock function

Jedną rzeczą, którą musisz wiedzieć (co zajęło mi trochę czasu, aby to rozgryźć) jest to, że nie możesz wywołać jest.mock () w teście; musisz to nazwać na najwyższym poziomie modułu. Możesz jednak wywołać mockImplementation () w ramach poszczególnych testów, jeśli chcesz skonfigurować różne próby dla różnych testów.

Andy
źródło
5

Odpowiedź na pytanie jest już udzielona, ​​ale możesz rozwiązać go w następujący sposób:

dependency.js

const doSomething = (x) => x
export default doSomething;

myModule.js:

import doSomething from "./dependency";

export default (x) => doSomething(x * 2);

myModule.spec.js:

jest.mock('../dependency');
import doSomething from "../dependency";
import myModule from "../myModule";

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    doSomething.mockImplementation((x) => x * 10)

    myModule(2);

    expect(doSomething).toHaveBeenCalledWith(4);
    console.log(myModule(2)) // 40
  });
});
Szczupły
źródło
Ale „wymaga” to składnia CommonJS - OP pytał o moduły ES6
Andy
@ Dziękuję za komentarz, zaktualizowałem swoją odpowiedź. BTW to samo w logice.
Slim
2

Rozwiązałem to w inny sposób. Załóżmy, że masz plik dependence.js

export const myFunction = () => { }

Oprócz tego tworzę plik depdency.mock.js z następującą zawartością:

export const mockFunction = jest.fn();

jest.mock('dependency.js', () => ({ myFunction: mockFunction }));

i w teście, zanim zaimportuję plik, którego używam zależności, używam:

import { mockFunction } from 'dependency.mock'
import functionThatCallsDep from './tested-code'

it('my test', () => {
    mockFunction.returnValue(false);

    functionThatCallsDep();

    expect(mockFunction).toHaveBeenCalled();

})
Felipe Leusin
źródło