วิธีการจำลองฟังก์ชันที่มีชื่อที่นำเข้าใน Jest เมื่อโมดูลไม่ถูกล็อค


120

ฉันมีโมดูลต่อไปนี้ที่ฉันกำลังพยายามทดสอบใน Jest:

// myModule.js

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

export function testFn() {
  otherFn();

  // do other things
}

ดังที่แสดงไว้ข้างต้นก็ส่งออกฟังก์ชั่นบางชื่อและที่สำคัญการใช้งานtestFnotherFn

ล้อเล่นเมื่อผมเขียนทดสอบหน่วยของฉันสำหรับtestFnผมต้องการที่จะเยาะเย้ยotherFnการทำงานเพราะผมไม่ต้องการให้เกิดข้อผิดพลาดในการส่งผลกระทบต่อการทดสอบหน่วยของฉันotherFn testFnปัญหาของฉันคือฉันไม่แน่ใจว่าวิธีที่ดีที่สุด:

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

ขอความช่วยเหลือ / ความเข้าใจใด ๆ


7
ฉันจะไม่ทำแบบนี้ โดยทั่วไปแล้วการล้อเลียนไม่ใช่สิ่งที่คุณต้องการทำอยู่แล้ว และหากคุณต้องการเยาะเย้ยอะไรบางอย่าง (เนื่องจากการเรียกเซิร์ฟเวอร์ / ฯลฯ ) คุณควรแยกotherFnออกเป็นโมดูลแยกต่างหากและเยาะเย้ยสิ่งนั้น
kentcdodds

2
ฉันยังทดสอบด้วยวิธีการเดียวกันกับที่ @jrubins ใช้ ทดสอบพฤติกรรมfunction Aว่าใครโทรมาfunction Bแต่ฉันไม่ต้องการดำเนินการใช้งานจริงfunction Bเพราะฉันต้องการทดสอบตรรกะที่ใช้งานในfunction A
jplaza

48
@kentcdodds คุณช่วยอธิบายความหมายของคำว่า "การล้อเลียนโดยทั่วไปไม่ใช่สิ่งที่คุณต้องการทำ" ดูเหมือนจะเป็นคำสั่งที่ค่อนข้างกว้าง (กว้างเกินไป?) เนื่องจากการเยาะเย้ยเป็นสิ่งที่มักใช้กันโดยสันนิษฐานว่าเป็นเหตุผลที่ดี (อย่างน้อยก็บางประการ) คุณอาจหมายถึงว่าทำไมการล้อเลียนจึงไม่ดีที่นี่หรือคุณหมายถึงโดยทั่วไปจริงๆ?
Andrew Willems

2
บ่อยครั้งที่การล้อเลียนคือการทดสอบรายละเอียดการใช้งาน โดยเฉพาะอย่างยิ่งในระดับนี้จะนำไปสู่การทดสอบที่ไม่ได้ตรวจสอบความถูกต้องมากไปกว่าการทดสอบของคุณใช้งานได้จริง (ไม่ใช่ว่ารหัสของคุณใช้งานได้)
kentcdodds

3
สำหรับบันทึกตั้งแต่เขียนคำถามนี้เมื่อหลายปีก่อนฉันได้เปลี่ยนการล้อเลียนว่าอยากทำมากแค่ไหน (และอย่าล้อเลียนแบบนี้อีกต่อไป) ทุกวันนี้ฉันเห็นด้วยกับ @kentcdodds และปรัชญาการทดสอบของเขาเป็นอย่างมาก (และขอแนะนำบล็อกของเขาและ@testing-library/reactสำหรับเครื่องปฏิกรณ์ใด ๆ ที่นั่น) แต่ฉันรู้ว่านี่เป็นเรื่องที่ถกเถียงกัน
Jon Rubins

คำตอบ:


113

ใช้jest.requireActual()ภายในjest.mock()

jest.requireActual(moduleName)

ส่งคืนโมดูลจริงแทนการจำลองโดยข้ามการตรวจสอบทั้งหมดว่าโมดูลควรได้รับการนำไปใช้จำลองหรือไม่

ตัวอย่าง

ฉันชอบการใช้งานที่กระชับซึ่งคุณต้องการและกระจายภายในวัตถุที่ส่งคืน:

// 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')
  })
})

วิธีนี้ยังอ้างถึงในเอกสารคู่มือ Mocks ของ Jest (ใกล้ท้ายตัวอย่าง ):

เพื่อให้แน่ใจว่าการจำลองแบบแมนนวลและการนำไปใช้จริงยังคงตรงกันอยู่อาจเป็นประโยชน์ที่จะต้องใช้โมดูลจริงที่ใช้jest.requireActual(moduleName)ในการจำลองด้วยตนเองของคุณและแก้ไขด้วยฟังก์ชันจำลองก่อนที่จะส่งออก


4
Fwiw คุณสามารถทำให้สิ่งนี้กระชับยิ่งขึ้นได้โดยการลบreturnคำสั่งและตัดส่วนของฟังก์ชันลูกศรไว้ในวงเล็บ: เช่น jest.mock('./myModule', () => ({ ...jest.requireActual('./myModule'), otherFn: () => {}}))
Nick F

2
มันใช้งานได้ดี! นี่ควรเป็นคำตอบที่ได้รับการยอมรับ
JRJurman

2
...jest.requireActualใช้งานไม่ได้สำหรับฉันอย่างถูกต้องเพราะฉันมีชื่อแทนเส้นทางโดยใช้ babel .. ใช้งานได้ทั้งกับ...require.requireActualหรือหลังจากลบนามแฝงจากเส้นทาง
Tzahi Leh

1
คุณจะทดสอบสิ่งที่otherFunเรียกว่าในกรณีนี้อย่างไร? สมมติotherFn: jest.fn()
Stevula

1
@Stevula ฉันได้อัปเดตคำตอบเพื่อแสดงตัวอย่างการใช้งานจริง ผมแสดงmockReturnValueวิธีการที่ดีในการแสดงให้เห็นว่ารุ่นเยาะเย้ยจะได้รับการเรียกแทนของเดิม .toHaveBeenCalled()แต่ถ้าคุณจริงๆเพียงแค่ต้องการที่จะดูว่าจะได้รับการเรียกโดยไม่ต้องเข้าไปยุ่งเกี่ยวกับค่าตอบแทนที่คุณสามารถใช้การจับคู่ความตลกขบขัน
gfullam

37
import m from '../myModule';

ไม่ได้ผลสำหรับฉันฉันใช้:

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

m.otherFn = jest.fn();

5
คุณจะคืนค่าฟังก์ชันการทำงานดั้งเดิมของ otherFn ได้อย่างไรหลังจากการทดสอบเพื่อไม่ให้รบกวนการทดสอบอื่น
Aequitas

1
ฉันคิดว่าคุณสามารถกำหนดค่า jest เพื่อล้าง mocks หลังการทดสอบทุกครั้งได้หรือไม่? จากเอกสาร: "ตัวเลือกการกำหนดค่า clearMocks พร้อมใช้งานเพื่อล้าง mocks โดยอัตโนมัติระหว่างการทดสอบ" คุณสามารถตั้งค่าclearkMocks: truejest package.json config facebook.github.io/jest/docs/en/mock-function-api.html
Cole

2
หากนี่เป็นปัญหาดังกล่าวในการเปลี่ยนสถานะทั่วโลกคุณสามารถจัดเก็บฟังก์ชันการทำงานดั้งเดิมไว้ในตัวแปรการทดสอบบางประเภทและนำกลับมาได้หลังจากการทดสอบ
bobu

1
const เดิม; beforeAll (() => {original = m.otherFn; m.otherFn = jest.fn ();}) afterAll (() => {m.otherFn = original;}) ควรใช้งานได้ แต่ฉันไม่ได้ทดสอบ มัน
bobu

27

ดูเหมือนว่าฉันจะสายไปงานปาร์ตี้นี้ แต่ใช่เป็นไปได้

testFnเพียงแค่ต้องการที่จะเรียกใช้โมดูลotherFn

หากtestFnใช้โมดูลเพื่อเรียกotherFnใช้การส่งออกโมดูลสำหรับotherFnสามารถtestFnจำลองได้และจะเรียกการจำลอง


นี่คือตัวอย่างการทำงาน:

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

3
นี่เป็นแนวทางเวอร์ชัน ES6 ที่ใช้ใน Facebook และอธิบายโดยนักพัฒนาของ Facebook ที่อยู่ตรงกลางของโพสต์นี้
Brian Adams

แทนที่จะนำเข้า myModule เข้าสู่ตัวเองเพียงโทรexports.otherFn()
andrhamm

3
@andrhamm exportsไม่มีอยู่ใน ES6 การโทรใช้exports.otherFn()งานได้ในขณะนี้เนื่องจาก ES6 กำลังคอมไพล์ไปยังไวยากรณ์ของโมดูลก่อนหน้านี้ แต่จะหยุดทำงานเมื่อรองรับ ES6
Brian Adams

มีปัญหานี้อยู่ในขณะนี้และฉันแน่ใจว่าเคยพบปัญหานี้มาก่อน ฉันต้องลบภาระการส่งออก <methodname> เพื่อช่วยให้ต้นไม้สั่นและมันก็เสียการทดสอบหลายครั้ง ฉันจะดูว่ามันมีผลหรือเปล่า แต่ดูเหมือนแฮ็คมาก ฉันเจอปัญหานี้หลายครั้งและก็เหมือนกับคำตอบอื่น ๆ ที่ได้กล่าวไว้สิ่งต่างๆเช่น babel-plugin-rewire หรือดีกว่านั้นคือnpmjs.com/package/rewiremockซึ่งฉันค่อนข้างแน่ใจว่าสามารถทำได้ข้างต้นเช่นกัน
Astridax

เป็นไปได้หรือไม่ที่จะเยาะเย้ยมูลค่าที่ส่งคืนมาให้เยาะเย้ยการโยน? แก้ไข: คุณทำได้นี่คือวิธีที่stackoverflow.com/a/50656680/2548010
Big Money

10

รหัส Transpiled จะไม่อนุญาตให้ babel ดึงข้อมูลการผูกที่otherFn()อ้างถึง หากคุณใช้ฟังก์ชัน expession คุณควรจะสามารถใช้การเยาะเย้ย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();

แต่เป็น @kentcdodds otherFn()กล่าวถึงในการแสดงความคิดเห็นก่อนหน้านี้คุณอาจจะไม่ต้องการที่จะเยาะเย้ย แต่เพียงแค่เขียนข้อมูลจำเพาะใหม่otherFn()และล้อเลียนการเรียกที่จำเป็น

ตัวอย่างเช่นหากotherFn()กำลังส่งคำขอ http ...

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

ที่นี่คุณจะต้องการล้อเลียนhttp.getและอัปเดตคำยืนยันของคุณโดยอิงจากการนำไปใช้ที่เยาะเย้ยของคุณ

// myModule.test.js
jest.mock('http', () => ({
  get: jest.fn(() => {
    console.log('test');
  }),
}));

1
จะเกิดอะไรขึ้นถ้า otherFn และ testFn ถูกใช้โดยโมดูลอื่น ๆ คุณจะต้องตั้งค่า http mock ในไฟล์ทดสอบทั้งหมดที่ใช้ (แต่ลึกสแต็ก) โมดูลทั้ง 2 นั้นหรือไม่ นอกจากนี้หากคุณมีการทดสอบสำหรับ testFn อยู่แล้วทำไมไม่ทำการตัด testFn โดยตรงแทนที่จะเป็น http ในโมดูลที่ใช้ testFn
rickmed

1
ดังนั้นหากotherFnเสียการทดสอบทั้งหมดที่ขึ้นอยู่กับการทดสอบนั้นจะล้มเหลว นอกจากนี้หากotherFnมี 5 ifs อยู่ภายในคุณอาจต้องทดสอบว่าของคุณtestFnทำงานได้ดีสำหรับกรณีย่อยเหล่านั้นทั้งหมด คุณจะมีเส้นทางรหัสอื่น ๆ อีกมากมายให้ทดสอบตอนนี้
Totty.js

7

ฉันรู้ว่าสิ่งนี้ถูกถามมานานแล้ว แต่ฉันเพิ่งเจอสถานการณ์นี้และในที่สุดก็พบวิธีแก้ปัญหาที่ได้ผล ฉันเลยคิดว่าจะแบ่งปันที่นี่

สำหรับโมดูล:

// myModule.js

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

export function testFn() {
  otherFn();

  // do other things
}

คุณสามารถเปลี่ยนดังต่อไปนี้:

// myModule.js

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

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

  // do other things
}

ส่งออกเป็นค่าคงที่แทนที่จะเป็นฟังก์ชัน ฉันเชื่อว่าปัญหานี้เกี่ยวข้องกับการยกใน JavaScript และการใช้constเพื่อป้องกันพฤติกรรมนั้น

จากนั้นในการทดสอบของคุณคุณสามารถมีสิ่งต่อไปนี้:

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

ตอนนี้การล้อเลียนของคุณควรทำงานตามที่คุณคาดหวังตามปกติ


2

ฉันแก้ไขปัญหาของฉันด้วยการผสมผสานของคำตอบที่พบที่นี่:

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

0

นอกเหนือจากคำตอบแรกที่นี่คุณสามารถใช้babel-plugin-rewireเพื่อจำลองฟังก์ชันที่มีชื่อที่นำเข้าได้เช่นกัน คุณสามารถตรวจสอบส่วนนี้อย่างผิวเผินสำหรับ การเดินสายฟังก์ชันที่ตั้งชื่อใหม่

ประโยชน์ทันทีอย่างหนึ่งสำหรับสถานการณ์ของคุณที่นี่คือคุณไม่จำเป็นต้องเปลี่ยนวิธีการเรียกฟังก์ชันอื่นจากฟังก์ชันของคุณ


จะกำหนดค่า babel-plugin-rewire ให้ทำงานกับ node.js ได้อย่างไร?
Timur Gilauri

0

ตามคำตอบของ Brian Adamsนี่คือวิธีที่ฉันสามารถใช้แนวทางเดียวกันใน TypeScript ยิ่งไปกว่านั้นการใช้jest.doMock ()เป็นไปได้ที่จะจำลองฟังก์ชันโมดูลเฉพาะในการทดสอบบางไฟล์ทดสอบและจัดเตรียมการจำลองการใช้งานสำหรับแต่ละไฟล์

src / module.ts

import * as module from './module';

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

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

export { foo, bar };

ทดสอบ / 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');
  });
});

ทดสอบ / 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) };
  });
}

ฟังก์ชัน Mocking ของโมดูลจะถูกย้ายไปยังฟังก์ชันตัวช่วยที่mockModulePartiallyอยู่ในไฟล์แยกต่างหากเพื่อให้สามารถใช้งานจากไฟล์ทดสอบต่างๆได้ (ซึ่งโดยทั่วไปแล้วสามารถอยู่ในไดเร็กทอรีอื่น ๆ ได้) มันขึ้นอยู่กับexpect.getState().testPathการแก้ไขพา ธ ไปยังโมดูล ( modulePath) ที่ถูกเยาะเย้ย (ทำให้สัมพันธ์กับhelpers.tsที่มีmockModulePartially) mocksCreatorฟังก์ชั่นส่งผ่านเป็นอาร์กิวเมนต์ที่สองเพื่อmockModulePartiallyควรส่งคืน mocks ของโมดูล ฟังก์ชันนี้ได้รับoriginalModuleและจำลองการใช้งานสามารถเลือกพึ่งพาได้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.