Wie man Funktionen im selben Modul mit jest verspottet

74

UPDATE: Ich habe diesen und andere Ansätze in https://github.com/magicmark/jest-how-do-i-mock-x/blob/master/src/function-in-same-module/README zusammengefasst. md


Was ist der beste Weg, um das folgende Beispiel richtig zu verspotten?

Das Problem ist, dass nach dem Import fooder Verweis auf das Original nicht verspottet bleibt bar.

module.js:

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

module.test.js:

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

describe('module', () => {
    let barSpy;

    beforeEach(() => {
        barSpy = jest.spyOn(
            module,
            'bar'
        ).mockImplementation(jest.fn());
    });


    afterEach(() => {
        barSpy.mockRestore();
    });

    it('foo', () => {
        console.log(jest.isMockFunction(module.bar)); // outputs true

        module.bar.mockReturnValue('fake bar');

        console.log(module.bar()); // outputs 'fake bar';

        expect(module.foo()).toEqual('I am foo. bar is fake bar');
        /**
         * does not work! we get the following:
         *
         *  Expected value to equal:
         *    "I am foo. bar is fake bar"
         *  Received:
         *    "I am foo. bar is bar"
         */
    });
});

Vielen Dank!

EDIT: Ich könnte ändern:

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

zu

export function foo () {
    return `I am foo. bar is ${exports.bar()}`;
}

aber das ist p. hässlich meiner Meinung nach überall zu tun: /


UPDATE: Ich habe diesen und andere Ansätze in https://github.com/magicmark/jest-how-do-i-mock-x/blob/master/src/function-in-same-module/README zusammengefasst. md

Kennzeichen
quelle
2
Siehe diesen Issue-Thread auf der jestGH-Seite github.com/facebook/jest/issues/936#issuecomment-545080082
Nickofthyme

Antworten:

35

Das Problem scheint damit zu tun zu haben, wie Sie erwarten, dass der Umfang der Leiste gelöst wird.

Einerseits module.jsexportieren Sie in zwei Funktionen (anstelle eines Objekts, das diese beiden Funktionen enthält). Aufgrund der Art und Weise, wie Module exportiert werden, ist der Verweis auf den Container der exportierten Objekte so, exportswie Sie es erwähnt haben.

Auf der anderen Seite behandeln Sie Ihren Export (den Sie mit einem Alias ​​versehen haben module) wie ein Objekt, das diese Funktionen enthält und versucht, eine seiner Funktionen (die Funktionsleiste) zu ersetzen.

Wenn Sie sich Ihre foo-Implementierung genau ansehen, haben Sie tatsächlich einen festen Verweis auf die Balkenfunktion.

Wenn Sie glauben, die Balkenfunktion durch eine neue ersetzt zu haben, haben Sie gerade die Referenzkopie im Bereich Ihrer module.test.js ersetzt

Damit foo tatsächlich eine andere Version von bar verwendet, haben Sie zwei Möglichkeiten:

  1. Exportieren Sie in module.js eine Klasse oder eine Instanz, die sowohl die foo- als auch die bar-Methode enthält:

    Module.js:

    export class MyModule {
      function bar () {
        return 'bar';
      }
    
      function foo () {
        return `I am foo. bar is ${this.bar()}`;
      }
    }
    

    Beachten Sie die Verwendung dieses Schlüsselworts in der foo-Methode.

    Module.test.js:

    import { MyModule } from '../src/module'
    
    describe('MyModule', () => {
      //System under test :
      const sut:MyModule = new MyModule();
    
      let barSpy;
    
      beforeEach(() => {
          barSpy = jest.spyOn(
              sut,
              'bar'
          ).mockImplementation(jest.fn());
      });
    
    
      afterEach(() => {
          barSpy.mockRestore();
      });
    
      it('foo', () => {
          sut.bar.mockReturnValue('fake bar');
          expect(sut.foo()).toEqual('I am foo. bar is fake bar');
      });
    });
    
  2. Schreiben Sie, wie Sie sagten, die globale Referenz in den globalen exportsContainer neu. Dies ist kein empfohlener Weg, da Sie möglicherweise in anderen Tests seltsame Verhaltensweisen einführen, wenn Sie die Exporte nicht ordnungsgemäß auf den Ausgangszustand zurücksetzen.

John-Philip
quelle
35

Eine alternative Lösung kann darin bestehen, das Modul in eine eigene Codedatei zu importieren und die importierte Instanz aller exportierten Entitäten zu verwenden. So was:

import * as thisModule from './module';

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${thisModule.bar()}`;
}

Jetzt ist das Verspotten barwirklich einfach, da fooauch die exportierte Instanz von verwendet wird bar:

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

describe('module', () => {
    it('foo', () => {
        spyOn(module, 'bar').and.returnValue('fake bar');
        expect(module.foo()).toEqual('I am foo. bar is fake bar');
    });
});

Das Importieren des Moduls in seinen eigenen Code sieht seltsam aus, aber aufgrund der Unterstützung des ES6 für zyklische Importe funktioniert es wirklich reibungslos.

MostafaR
quelle
Dies funktionierte für mich mit den geringsten Auswirkungen auf vorhandenen Code und einfach zu befolgenden Tests.
Shiraz
Dies war auch für mich der einfachste Weg.
Borduhh
Sehr hilfreich. Vielen Dank.
Nick Law
10

fwiw, die Lösung, für die ich mich entschieden habe, war die Verwendung der Abhängigkeitsinjektion durch Festlegen eines Standardarguments.

Also würde ich mich ändern

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

zu

export function bar () {
    return 'bar';
}

export function foo (_bar = bar) {
    return `I am foo. bar is ${_bar()}`;
}

Dies ist keine grundlegende Änderung an der API meiner Komponente, und ich kann die Leiste in meinem Test leicht überschreiben, indem ich die folgenden Schritte ausführe

import { foo, bar } from '../src/module';

describe('module', () => {
    it('foo', () => {
        const dummyBar = jest.fn().mockReturnValue('fake bar');
        expect(foo(dummyBar)).toEqual('I am foo. bar is fake bar');
    });
});

Dies hat den Vorteil, dass auch der Testcode etwas besser ist :)

Kennzeichen
quelle
6
Ich bin im Allgemeinen kein Fan von Abhängigkeitsinjektion, da Sie Tests erlauben, die Schreibweise des Codes zu ändern.
Sean
10
Schöner Test, aber schlechter Code. Es ist keine gute Idee, Ihren Code zu ändern, da Sie keinen Weg finden, ihn zu testen. Wenn ich mir als Entwickler diesen Code anschaue, denke ich 100-mal darüber nach, warum eine bestimmte im Modul vorhandene Methode als Abhängigkeit von einer anderen Methode im selben Modul übergeben wurde.
Gaurav Kumar
7

Ich hatte das gleiche Problem und aufgrund der Flusenstandards des Projekts waren das Definieren einer Klasse oder das Umschreiben von Referenzen in den exportsOptionen keine Codeüberprüfung, auch wenn dies nicht durch die Flusendefinitionen verhindert wurde. Als praktikable Option bin ich auf das Babel-Rewire-Plugin gestoßen , das zumindest optisch viel sauberer ist. Während ich fand, dass dies in einem anderen Projekt verwendet wurde, auf das ich Zugriff hatte, bemerkte ich, dass es bereits in einer Antwort auf eine ähnliche Frage enthalten war, die ich hier verlinkt habe . Dies ist ein Ausschnitt, der für diese Frage (und ohne Verwendung von Spionen) angepasst wurde und aus der verknüpften Antwort als Referenz bereitgestellt wird (ich habe zusätzlich zum Entfernen von Spionen auch Semikolons hinzugefügt, da ich kein Heide bin):

import __RewireAPI__, * as module from '../module';

describe('foo', () => {
  it('calls bar', () => {
    const barMock = jest.fn();
    __RewireAPI__.__Rewire__('bar', barMock);
    
    module.foo();

    expect(bar).toHaveBeenCalledTimes(1);
  });
});

https://stackoverflow.com/a/45645229/6867420

Brandon Hunter
quelle
3
Dies sollte als Antwort akzeptiert werden. Das Plugin funktioniert einfach und es ist nicht erforderlich, Code außerhalb des Tests neu zu schreiben. TY
Andrew Rockwell
Vielen Dank dafür, wenn Sie sich in einer Babel-Umgebung befinden, ist dies die Antwort, nach der Sie suchen.
Lyle Underwood
2

Funktioniert bei mir:

cat moduleWithFunc.ts

export function funcA() {
 return export.funcB();
}
export function funcB() {
 return false;
}

cat moduleWithFunc.test.ts

import * as module from './moduleWithFunc';

describe('testFunc', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  afterEach(() => {
    module.funcB.mockRestore();
  });

  it.only('testCase', () => {
    // arrange
    jest.spyOn(module, 'funcB').mockImplementationOnce(jest.fn().mockReturnValue(true));

    // act
    const result = module.funcA();

    // assert
    expect(result).toEqual(true);
    expect(module.funcB).toHaveBeenCalledTimes(1);
  });
});
Сергей Галкин
quelle
1

Wenn Sie Ihre Exporte definieren, können Sie Ihre Funktionen als Teil des Exportobjekts referenzieren. Dann können Sie die Funktionen in Ihren Mocks einzeln überschreiben. Dies liegt daran, wie der Import als Referenz und nicht als Kopie funktioniert.

module.js:

exports.bar () => {
    return 'bar';
}

exports.foo () => {
    return `I am foo. bar is ${exports.bar()}`;
}

module.test.js:

describe('MyModule', () => {

  it('foo', () => {
    let module = require('./module')
    module.bar = jest.fn(()=>{return 'fake bar'})

    expect(module.foo()).toEqual('I am foo. bar is fake bar');
  });

})
Sean
quelle
Ich mag das, aber für mich explodiert es im Produktionspaket. exports is undefiend
theUtherSide