So verspotten Sie importierte benannte Funktionen in Jest, wenn das Modul nicht entsperrt ist

100

Ich habe das folgende Modul, das ich in Jest testen möchte:

// myModule.js

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

export function testFn() {
  otherFn();

  // do other things
}

Wie oben gezeigt, werden einige benannte Funktionen exportiert und vor allem testFnverwendet otherFn.

Wenn ich im Scherz meinen Komponententest für schreibe testFn, möchte ich die otherFnFunktion verspotten , da ich nicht möchte, dass Fehler otherFnmeinen Komponententest beeinflussen testFn. Mein Problem ist, dass ich nicht sicher bin, wie ich das am besten machen kann:

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

Jede Hilfe / Einsicht wird geschätzt.

Jon Rubins
quelle
7
Ich würde das nicht tun. Verspotten ist im Allgemeinen sowieso nichts, was Sie tun möchten. Und wenn Sie etwas verspotten müssen (aufgrund von Serveraufrufen / etc.), Sollten Sie einfach otherFnin ein separates Modul extrahieren und das verspotten.
Kentcdodds
2
Ich teste auch mit dem gleichen Ansatz, den @jrubins verwendet. Testverhalten von function Awem anruft, function Baber ich möchte nicht die eigentliche Implementierung von ausführen, function Bweil ich nur die infunction A
jplaza
39
@kentcdodds, Könnten Sie klarstellen, was Sie unter "Spott ist im Allgemeinen sowieso nichts, was Sie tun möchten" verstehen? Das scheint eine ziemlich breite (zu breite?) Aussage zu sein, da Spott sicherlich etwas ist, das oft verwendet wird, vermutlich aus (zumindest einigen) guten Gründen. Beziehen Sie sich vielleicht darauf, warum Spott hier vielleicht nicht gut ist , oder meinen Sie das wirklich allgemein?
Andrew Willems
2
Oft werden beim Verspotten Details der Implementierung getestet. Insbesondere auf dieser Ebene führt dies zu Tests, die nicht viel mehr validieren als die Tatsache, dass Ihre Tests funktionieren (nicht, dass Ihr Code funktioniert).
Kentcdodds
2
Seit ich diese Frage vor Jahren geschrieben habe, habe ich meine Einstellung dahingehend geändert, wie viel Spott ich gerne machen würde (und mache nicht mehr so ​​Spott). Heutzutage stimme ich @kentcdodds und seiner Testphilosophie sehr zu (und empfehle seinen Blog und @testing-library/reactalle Leser da draußen sehr), aber ich weiß, dass dies ein umstrittenes Thema ist.
Jon Rubins

Antworten:

93

Einsatz im jest.requireActual()Innernjest.mock()

jest.requireActual(moduleName)

Gibt das eigentliche Modul anstelle eines Mocks zurück und umgeht alle Überprüfungen, ob das Modul eine Mock-Implementierung erhalten soll oder nicht.

Beispiel

Ich bevorzuge diese prägnante Verwendung, wenn Sie sie benötigen und innerhalb des zurückgegebenen Objekts verbreiten:

// myModule.test.js

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

describe(...)

Auf diese Methode wird auch in der Dokumentation zu Jest's Manual Mocks (gegen Ende der Beispiele ) verwiesen :

Um sicherzustellen, dass ein manueller Mock und seine tatsächliche Implementierung synchron bleiben, kann es hilfreich sein, das reale Modul jest.requireActual(moduleName)in Ihrem manuellen Mock zu verwenden und es vor dem Export mit Mock-Funktionen zu ändern.

Gfullam
quelle
4
Fwiw, Sie können dies noch präziser gestalten, indem Sie die returnAnweisung entfernen und den Pfeilfunktionskörper in Klammern setzen: z. jest.mock('./myModule', () => ({ ...jest.requireActual('./myModule'), otherFn: () => {}}))
Nick F
2
Das hat super geklappt! Dies sollte die akzeptierte Antwort sein.
JRJurman
Ist es überhaupt möglich, jest.mock () in dieser Lösung innerhalb der Testdefinition und nicht auf der Ebene der Importanweisungen (oberste Ebene in der Datei) zu haben?
Denu
2
...jest.requireActualhat bei mir nicht richtig funktioniert, weil ich Pfad-Aliasing mit babel habe. Funktioniert entweder mit ...require.requireActualoder nach dem Entfernen des Aliasing vom Pfad
Tzahi Leh
1
funktioniert super danke
Daniel Gardiner
34
import m from '../myModule';

Funktioniert bei mir nicht, ich habe verwendet:

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

m.otherFn = jest.fn();
Bobu
quelle
4
Wie würden Sie die ursprüngliche Funktionalität von otherFn nach dem Test wiederherstellen, damit andere Tests nicht beeinträchtigt werden?
Aequitas
1
Ich denke, Sie können Jest so konfigurieren, dass nach jedem Test Mocks gelöscht werden? Aus den Dokumenten: "Die Konfigurationsoption clearMocks ist verfügbar, um Mocks zwischen Tests automatisch zu löschen." Sie können clearkMocks: truejest package.json config festlegen. facebook.github.io/jest/docs/en/mock-function-api.html
Cole
2
Wenn dies ein solches Problem ist, um den globalen Status zu ändern, können Sie die ursprüngliche Funktionalität jederzeit in einer
Testvariablen
1
const original; beforeAll (() => {original = m.otherFn; m.otherFn = jest.fn ();}) afterAll (() => {m.otherFn = original;}) sollte funktionieren, ich habe es jedoch nicht getestet it
Bobu
23

Sieht so aus, als wäre ich zu spät zu dieser Party, aber ja, das ist möglich.

testFnmuss nur otherFn mit dem Modul anrufen .

Wenn testFndas Modul zum Aufrufen verwendet otherFnwird, kann der Modulexport für otherFnverspottet werden und testFnruft den Mock auf.


Hier ist ein Arbeitsbeispiel:

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
quelle
2
Dies ist im Wesentlichen eine ES6-Version des bei Facebook verwendeten Ansatzes, der von einem Facebook-Entwickler in der Mitte dieses Beitrags beschrieben wird .
Brian Adams
anstatt myModule in sich selbst zu importieren, rufen exports.otherFn()
Sie
2
@andrhamm exportsexistiert in ES6 nicht. Das Aufrufen exports.otherFn()funktioniert derzeit, da ES6 mit einer früheren Modulsyntax kompiliert wird. Es wird jedoch unterbrochen, wenn ES6 nativ unterstützt wird.
Brian Adams
Ich habe genau dieses Problem und bin mir sicher, dass ich dieses Problem schon einmal erlebt habe. Ich musste eine Menge Exporte entfernen. <Methodenname>, um das Baumschütteln zu erleichtern, und es hat viele Tests gebrochen. Ich werde sehen, ob dies irgendeinen Effekt hat, aber es scheint so hackig. Ich bin mehrmals auf dieses Problem gestoßen und wie andere Antworten gesagt haben, Dinge wie babel-plugin-rewire oder noch besser npmjs.com/package/rewiremock, von denen ich ziemlich sicher bin, dass sie das auch können.
Astridax
Ist es möglich, anstatt einen Rückgabewert zu verspotten, einen Wurf zu verspotten? Bearbeiten: Sie können, hier ist, wie stackoverflow.com/a/50656680/2548010
Big Money
9

Mit dem transpilierten Code kann babel die Bindung, otherFn()auf die verwiesen wird, nicht abrufen . Wenn Sie eine Funktionsexpression verwenden, sollten Sie in der Lage sein, eine Verspottung zu erreichen 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();

Aber wie @kentcdodds im vorherigen Kommentar erwähnt, möchten Sie wahrscheinlich nicht verspotten otherFn(). Schreiben Sie einfach eine neue Spezifikation für otherFn()und verspotten Sie alle erforderlichen Anrufe.

Wenn Sie otherFn()beispielsweise eine http-Anfrage stellen ...

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

Hier möchten Sie http.getIhre Behauptungen basierend auf Ihren verspotteten Implementierungen verspotten und aktualisieren.

// myModule.test.js
jest.mock('http', () => ({
  get: jest.fn(() => {
    console.log('test');
  }),
}));
Vutran
quelle
1
Was ist, wenn otherFn und testFn von mehreren anderen Modulen verwendet werden? Müssen Sie den http-Mock in allen Testdateien einrichten, die diese beiden Module verwenden (egal wie tief der Stapel ist)? Wenn Sie bereits einen Test für testFn haben, können Sie testFn in den Modulen, die testFn verwenden, direkt anstelle von http stubben.
Rickmed
1
Wenn dies nicht der Fall otherFnist, werden alle Tests, die von diesem abhängen, nicht bestanden. Auch wenn otherFn5 ifs enthalten sind, müssen Sie möglicherweise testen, ob Ihr testFnfür alle diese Unterfälle gut funktioniert. Sie müssen jetzt so viele weitere Codepfade testen.
Totty.js
4

Ich weiß, dass dies vor langer Zeit gefragt wurde, aber ich bin gerade in diese Situation geraten und habe endlich eine Lösung gefunden, die funktionieren würde. Also dachte ich, ich würde hier teilen.

Für das Modul:

// myModule.js

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

export function testFn() {
  otherFn();

  // do other things
}

Sie können Folgendes ändern:

// myModule.js

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

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

  // do other things
}

Exportieren sie als Konstanten anstelle von Funktionen. Ich glaube, das Problem hat mit dem Heben in JavaScript zu tun und die Verwendung constverhindert dieses Verhalten.

Dann können Sie in Ihrem Test so etwas wie das Folgende haben:

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

Ihre Mocks sollten jetzt so funktionieren, wie Sie es normalerweise erwarten würden.

Jack Kinsey
quelle
2

Ich habe mein Problem mit einer Mischung der Antworten gelöst, die ich hier gefunden habe:

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
quelle
0

Zusätzlich zur ersten Antwort hier können Sie babel-plugin-rewire verwenden , um auch importierte benannte Funktionen zu verspotten. Sie können den Abschnitt oberflächlich auf die Neuverdrahtung benannter Funktionen überprüfen .

Einer der unmittelbaren Vorteile für Ihre Situation besteht darin, dass Sie nicht ändern müssen, wie Sie die andere Funktion von Ihrer Funktion aus aufrufen.

Schrödingers Code
quelle
Wie konfiguriere ich babel-plugin-rewire für die Arbeit mit node.js?
Timur Gilauri