Jak udawać zaimportowaną nazwaną funkcję w Jest, gdy moduł jest odblokowany

120

Mam następujący moduł, który próbuję przetestować w Jest:

// myModule.js

export function otherFn() {
  console.log('do something');
}

export function testFn() {
  otherFn();

  // do other things
}

Jak pokazano powyżej, eksportuje niektóre nazwane funkcje i, co ważne, testFnużywa otherFn.

W Jest, kiedy piszę mój test jednostkowy dla testFn, chcę mockować otherFnfunkcję, ponieważ nie chcę, aby błędy otherFnwpływały na mój test jednostkowy testFn. Mój problem polega na tym, że nie jestem pewien, jak najlepiej to zrobić:

// myModule.test.js
jest.unmock('myModule');

import { testFn, otherFn } from 'myModule';

describe('test category', () => {
  it('tests something about testFn', () => {
    // I want to mock "otherFn" here but can't reassign
    // a.k.a. can't do otherFn = jest.fn()
  });
});

Każda pomoc / wgląd jest mile widziany.

Jon Rubins
źródło
7
Nie zrobiłbym tego. Na ogół kpiny i tak nie są czymś, co chcesz robić. A jeśli chcesz coś mockować (ze względu na wywołania serwera / itp.), Powinieneś po prostu rozpakować otherFndo oddzielnego modułu i udawać.
kentcdodds
2
Testuję również z tym samym podejściem, którego używa @jrubins. Testuj zachowanie function Akto dzwoni, function Bale nie chcę wykonywać prawdziwej implementacji, function Bponieważ chcę tylko przetestować logikę zaimplementowaną wfunction A
jplaza
48
@kentcdodds, Czy mógłbyś wyjaśnić, co masz na myśli, mówiąc „wyśmiewanie na ogół nie jest czymś, co i tak chcesz robić”? Wydaje się, że jest to dość szerokie (zbyt szerokie?) Stwierdzenie, ponieważ kpiny jest z pewnością czymś, co jest często używane, prawdopodobnie z (przynajmniej niektórych) dobrych powodów. A więc może odnosisz się do tego, dlaczego kpiny mogą nie być tutaj dobre , czy też naprawdę masz na myśli ogólnie?
Andrew Willems,
2
Często kpiny to testowanie szczegółów implementacji. Szczególnie na tym poziomie prowadzi to do testów, które tak naprawdę nie potwierdzają wiele więcej niż fakt, że twoje testy działają (a nie że twój kod działa).
kentcdodds,
3
Dla przypomnienia, odkąd napisałem to pytanie lata temu, od tego czasu zmieniłem zdanie na temat tego, jak bardzo chciałbym kpić (i nie rób już tego kpiny). Obecnie bardzo zgadzam się z @kentcdodds i jego filozofią testowania (i bardzo polecam jego bloga i @testing-library/reactkażdemu, kto tam jest), ale wiem, że jest to kontrowersyjny temat.
Jon Rubins

Odpowiedzi:

113

Użyj jest.requireActual()wewnątrzjest.mock()

jest.requireActual(moduleName)

Zwraca rzeczywisty moduł zamiast makiety, pomijając wszystkie sprawdzenia, czy moduł powinien otrzymać fałszywą implementację, czy nie.

Przykład

Wolę to zwięzłe użycie, gdy potrzebujesz i rozpowszechniasz w zwracanym obiekcie:

// myModule.test.js

jest.mock('./myModule.js', () => (
  {
    ...(jest.requireActual('./myModule.js')),
    otherFn: jest.fn()
  }
))

import { otherFn } from './myModule.js'

describe('test category', () => {
  it('tests something about otherFn', () => {
    otherFn.mockReturnValue('foo')
    expect(otherFn()).toBe('foo')
  })
})

Ta metoda jest również opisana w dokumentacji Jest's Manual Mocks (pod koniec przykładów ):

Aby upewnić się, że ręczna makieta i jej rzeczywista implementacja pozostają zsynchronizowane, warto wymagać, aby rzeczywisty moduł używał jest.requireActual(moduleName)w ręcznym makiecie i poprawił go za pomocą funkcji makiety przed wyeksportowaniem.

gfullam
źródło
4
Fwiw, możesz uczynić to jeszcze bardziej zwięzłym, usuwając returninstrukcję i umieszczając treść funkcji strzałki w nawiasach: np. jest.mock('./myModule', () => ({ ...jest.requireActual('./myModule'), otherFn: () => {}}))
Nick F
2
To działało świetnie! To powinna być akceptowana odpowiedź.
JRJurman
2
...jest.requireActualnie działało poprawnie, ponieważ mam aliasowanie ścieżki za pomocą babel .. Działa z ...require.requireActuallub po usunięciu aliasingu ze ścieżki
Tzahi Leh
1
Jak przetestowałbyś to, co otherFunzostało wywołane w tym przypadku? ZakładającotherFn: jest.fn()
Stevula
1
@Stevula Zaktualizowałem moją odpowiedź, aby pokazać prawdziwy przykład użycia. Pokazuję mockReturnValuemetodę, aby lepiej zademonstrować, że zamiast oryginału jest wywoływana wersja symulowana, ale jeśli naprawdę chcesz tylko sprawdzić, czy została wywołana bez potwierdzania wartości zwracanej, możesz użyć dopasowania jest .toHaveBeenCalled().
gfullam
37
import m from '../myModule';

U mnie nie działa, użyłem:

import * as m from '../myModule';

m.otherFn = jest.fn();
bobu
źródło
5
Jak przywrócić oryginalną funkcjonalność innego Fn po teście, aby nie kolidował z innymi testami?
Aequitas
1
Myślę, że możesz skonfigurować żart, aby usunąć makiety po każdym teście? Z dokumentacji: "Dostępna jest opcja konfiguracyjna clearMocks, która umożliwia automatyczne usuwanie mocków między testami.". Możesz ustawić clearkMocks: truejest package.json config. facebook.github.io/jest/docs/en/mock-function-api.html
Cole
2
jeśli to jest taki problem, aby zmienić stan globalny, zawsze możesz zapisać oryginalną funkcjonalność w jakiejś zmiennej testowej i przywrócić ją po teście
bobu
1
const oryginał; beforeAll (() => {original = m.otherFn; m.otherFn = jest.fn ();}) afterAll (() => {m.otherFn = original;}) powinno działać, jednak nie testowałem it
bobu
27

Wygląda na to, że spóźniłem się na to przyjęcie, ale tak, jest to możliwe.

testFnwystarczy zadzwonić otherFn za pomocą modułu .

Jeśli testFnużywa modułu do wywołania, otherFnwówczas eksport modułu dla otherFnmoże być mockowany i testFnwywoła model.


Oto działający przykład:

myModule.js

import * as myModule from './myModule';  // import myModule into itself

export function otherFn() {
  return 'original value';
}

export function testFn() {
  const result = myModule.otherFn();  // call otherFn using the module

  // do other things

  return result;
}

myModule.test.js

import * as myModule from './myModule';

describe('test category', () => {
  it('tests something about testFn', () => {
    const mock = jest.spyOn(myModule, 'otherFn');  // spy on otherFn
    mock.mockReturnValue('mocked value');  // mock the return value

    expect(myModule.testFn()).toBe('mocked value');  // SUCCESS

    mock.mockRestore();  // restore otherFn
  });
});
Brian Adams
źródło
3
Jest to zasadniczo wersja ES6 podejścia stosowanego na Facebooku i opisana przez twórcę Facebooka w środku tego postu .
Brian Adams,
zamiast importować myModule do samego siebie, po prostu zadzwońexports.otherFn()
andrhamm
3
@andrhamm exportsnie istnieje w ES6. Wywołanie exports.otherFn()działa teraz, ponieważ ES6 jest kompilowane do wcześniejszej składni modułu, ale zepsuje się, gdy ES6 będzie obsługiwane natywnie.
Brian Adams
Mam teraz dokładnie ten problem i jestem pewien, że napotkałem ten problem już wcześniej. Musiałem usunąć ładunek eksportu. <nazwa_metody>, aby drzewo mogło się trząść i wiele testów zepsuło. Zobaczę, czy to przyniesie jakikolwiek efekt, ale wydaje się, że jest to takie hakerskie. Napotkałem ten problem wiele razy i jak powiedziały inne odpowiedzi, rzeczy takie jak babel-plugin-rewire lub nawet lepiej, npmjs.com/package/rewiremock, który jestem całkiem pewien, że może to zrobić również powyżej.
Astridax
Czy można zamiast mockować zwracaną wartość, udawać rzut? Edycja: możesz, oto jak stackoverflow.com/a/50656680/2548010
Big Money
10

Transpiled kod nie pozwoli Babel na pobranie powiązania, do którego otherFn()się odnosi. Jeśli używasz wyrażenia funkcji, powinieneś być w stanie uzyskać mockowanie otherFn().

// myModule.js
exports.otherFn = () => {
  console.log('do something');
}

exports.testFn = () => {
  exports.otherFn();

  // do other things
}

 

// myModule.test.js
import m from '../myModule';

m.otherFn = jest.fn();

Ale jak @kentcdodds wspomniał w poprzednim komentarzu, prawdopodobnie nie chciałbyś kpić otherFn(). Zamiast tego po prostu napisz nową specyfikację otherFn()i udawaj wszelkie niezbędne wywołania, które wykonuje.

Na przykład, jeśli otherFn()wysyła żądanie http ...

// myModule.js
exports.otherFn = () => {
  http.get('http://some-api.com', (res) => {
    // handle stuff
  });
};

Tutaj chciałbyś kpić http.geti aktualizować swoje potwierdzenia na podstawie twoich udawanych implementacji.

// myModule.test.js
jest.mock('http', () => ({
  get: jest.fn(() => {
    console.log('test');
  }),
}));
vutran
źródło
1
a co jeśli otherFn i testFn są używane przez kilka innych modułów? czy musiałbyś ustawić makietę http we wszystkich plikach testowych, które używają (jakkolwiek głęboko stosu) tych 2 modułów? Ponadto, jeśli masz już test dla testFn, dlaczego nie odgiąć bezpośrednio testFn zamiast http w modułach używających testFn?
ricked
1
więc jeśli otherFnjest uszkodzony, nie przejdzie wszystkich testów, które zależą od tego. Również jeśli otherFnmasz 5 ifów w środku, być może będziesz musiał sprawdzić, czy twój testFndziała dobrze dla wszystkich tych przypadków podrzędnych. Będziesz mieć teraz o wiele więcej ścieżek kodu do przetestowania.
Totty.js
7

Wiem, że zadawano to dawno temu, ale właśnie natknąłem się na tę właśnie sytuację i w końcu znalazłem rozwiązanie, które zadziała. Więc pomyślałem, że podzielę się tutaj.

Dla modułu:

// myModule.js

export function otherFn() {
  console.log('do something');
}

export function testFn() {
  otherFn();

  // do other things
}

Możesz zmienić na następujące:

// myModule.js

export const otherFn = () => {
  console.log('do something');
}

export const testFn = () => {
  otherFn();

  // do other things
}

eksportowanie ich jako stałych zamiast funkcji. Uważam, że problem ma związek z podnoszeniem JavaScript i używanie constzapobiega temu zachowaniu.

Następnie w swoim teście możesz mieć coś takiego:

import * as myModule from 'myModule';


describe('...', () => {
  jest.spyOn(myModule, 'otherFn').mockReturnValue('what ever you want to return');

  // or

  myModule.otherFn = jest.fn(() => {
    // your mock implementation
  });
});

Twoje makiety powinny teraz działać tak, jak zwykle byś oczekiwał.

Jack Kinsey
źródło
2

Rozwiązałem swój problem za pomocą kombinacji odpowiedzi, które znalazłem tutaj:

myModule.js

import * as myModule from './myModule';  // import myModule into itself

export function otherFn() {
  return 'original value';
}

export function testFn() {
  const result = myModule.otherFn();  // call otherFn using the module

  // do other things

  return result;
}

myModule.test.js

import * as myModule from './myModule';

describe('test category', () => {
  let otherFnOrig;

  beforeAll(() => {
    otherFnOrig = myModule.otherFn;
    myModule.otherFn = jest.fn();
  });

  afterAll(() => {
    myModule.otherFn = otherFnOrig;
  });

  it('tests something about testFn', () => {
    // using mock to make the tests
  });
});
demaroar
źródło
0

Oprócz pierwszej odpowiedzi tutaj, możesz użyć babel-plugin-rewire do mockowania zaimportowanej nazwanej funkcji. Możesz przejrzeć tę sekcję powierzchownie, aby uzyskać informacje o ponownym okablowaniu funkcji .

Jedną z bezpośrednich korzyści dla twojej sytuacji jest to, że nie musisz zmieniać sposobu wywoływania drugiej funkcji ze swojej funkcji.

kod Schrodingera
źródło
Jak skonfigurować babel-plugin-rewire do pracy z node.js?
Timur Gilauri
0

Opierając się na odpowiedzi Briana Adamsa, w ten sposób mogłem zastosować to samo podejście w TypeScript. Co więcej, za pomocą jest.doMock () można mockować funkcje modułu tylko w niektórych specyficznych testach pliku testowego i dostarczać dla każdego z nich indywidualne mockowe implementacje.

src / module.ts

import * as module from './module';

function foo(): string {
  return `foo${module.bar()}`;
}

function bar(): string {
  return 'bar';
}

export { foo, bar };

test / module.test.ts

import { mockModulePartially } from './helpers';

import * as module from '../src/module';

const { foo } = module;

describe('test suite', () => {
  beforeEach(function() {
    jest.resetModules();
  });

  it('do not mock bar 1', async() => {
    expect(foo()).toEqual('foobar');
  });

  it('mock bar', async() => {
    mockModulePartially('../src/module', () => ({
      bar: jest.fn().mockImplementation(() => 'BAR')
    }));
    const module = await import('../src/module');
    const { foo } = module;
    expect(foo()).toEqual('fooBAR');
  });

  it('do not mock bar 2', async() => {
    expect(foo()).toEqual('foobar');
  });
});

test / helpers.ts

export function mockModulePartially(
  modulePath: string,
  mocksCreator: (originalModule: any) => Record<string, any>
): void {
  const testRelativePath = path.relative(path.dirname(expect.getState().testPath), __dirname);
  const fixedModulePath = path.relative(testRelativePath, modulePath);
  jest.doMock(fixedModulePath, () => {
    const originalModule = jest.requireActual(fixedModulePath);
    return { ...originalModule, ...mocksCreator(originalModule) };
  });
}

Mockowanie funkcji modułu jest przenoszone do funkcji pomocniczej mockModulePartiallyznajdującej się w oddzielnym pliku, dzięki czemu może być używana z różnych plików testowych (które zwykle mogą znajdować się w innych katalogach). Polega na expect.getState().testPathnaprawieniu ścieżki do modułu ( modulePath), który jest mockowany (uczynienie go względnym do helpers.tszawierającego mockModulePartially). mocksCreatorfunkcja przekazana jako drugi argument mockModulePartiallypowinna zwrócić makiety modułu. Ta funkcja originalModulemoże opcjonalnie na niej polegać.

ezze
źródło