Cách giả lập hàm có tên đã nhập trong Jest khi mô-đun được mở khóa


111

Tôi có mô-đun sau mà tôi đang cố gắng kiểm tra trong Jest:

// myModule.js

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

export function testFn() {
  otherFn();

  // do other things
}

Như được hiển thị ở trên, nó xuất một số hàm được đặt tên và testFnsử dụng quan trọng otherFn.

Trong Jest khi tôi đang viết bài kiểm tra đơn vị của mình cho testFn, tôi muốn mô phỏng otherFnhàm vì tôi không muốn các lỗi otherFnảnh hưởng đến bài kiểm tra đơn vị của mình testFn. Vấn đề của tôi là tôi không chắc cách tốt nhất để làm điều đó:

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

Bất kỳ trợ giúp / cái nhìn sâu sắc được đánh giá cao.


7
Tôi sẽ không làm điều này. Chế giễu thường không phải là điều bạn muốn làm. Và nếu bạn cần mô phỏng điều gì đó (do thực hiện các cuộc gọi máy chủ / v.v.) thì bạn chỉ nên trích xuất otherFnvào một mô-đun riêng biệt và mô phỏng điều đó.
kentcdodds

2
Tôi cũng đang thử nghiệm với cùng một phương pháp mà @jrubins sử dụng. Kiểm tra hành vi của function Angười gọi function Bnhưng tôi không muốn thực hiện việc thực hiện thực sự của function Bbởi vì tôi muốn chỉ kiểm tra logic thực hiện trongfunction A
jplaza

44
@kentcdodds, Bạn có thể làm rõ ý của bạn khi "Chế giễu thường không phải là điều bạn muốn làm."? Đó dường như là một tuyên bố khá rộng (quá rộng?), Vì chế nhạo chắc chắn là thứ thường được sử dụng, có lẽ vì (ít nhất là một số) lý do chính đáng. Vì vậy, có lẽ bạn đang đề cập đến lý do tại sao chế nhạo có thể không tốt ở đây , hay bạn thực sự có ý nói chung?
Andrew Willems

2
Thường chế nhạo là kiểm tra chi tiết triển khai. Đặc biệt là ở cấp độ này, nó dẫn đến các bài kiểm tra không thực sự xác thực nhiều hơn việc các bài kiểm tra của bạn hoạt động (không phải là mã của bạn hoạt động).
kentcdodds

3
Đối với hồ sơ, kể từ khi viết câu hỏi này nhiều năm trước, tôi đã thay đổi giai điệu của mình về mức độ chế giễu mà tôi muốn làm (và không chế giễu như thế này nữa). Những ngày này, tôi rất đồng ý với @kentcdodds và triết lý thử nghiệm của anh ấy (và rất khuyến khích blog của anh ấy và @testing-library/reactcho bất kỳ Reacter nào ngoài đó) nhưng tôi biết đây là một chủ đề gây tranh cãi.
Jon Rubins

Câu trả lời:


100

Sử dụng jest.requireActual()bên trongjest.mock()

jest.requireActual(moduleName)

Trả về mô-đun thực thay vì mô-đun, bỏ qua tất cả các kiểm tra về việc liệu mô-đun có nhận được triển khai mô-đun hay không.

Thí dụ

Tôi thích cách sử dụng ngắn gọn này khi bạn yêu cầu và lan truyền trong đối tượng trả về:

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

Phương pháp này cũng được tham chiếu trong tài liệu Mô hình thủ công của Jest (gần cuối Ví dụ ):

Để đảm bảo rằng mô hình thủ công và việc triển khai thực tế của nó luôn đồng bộ, có thể hữu ích khi yêu cầu mô-đun thực sử dụng jest.requireActual(moduleName)trong mô hình thủ công của bạn và sửa đổi nó bằng các chức năng mô phỏng trước khi xuất nó.


4
Rất tiếc, bạn có thể làm cho điều này ngắn gọn hơn nữa bằng cách loại bỏ returncâu lệnh và gói phần thân hàm mũi tên trong dấu ngoặc đơn: ví dụ: jest.mock('./myModule', () => ({ ...jest.requireActual('./myModule'), otherFn: () => {}}))
Nick F

2
Điều này làm việc tuyệt vời! Đây phải là câu trả lời được chấp nhận.
JRJurman

2
...jest.requireActualkhông làm việc cho tôi đúng bởi vì tôi có con đường răng cưa sử dụng babel .. trình hoặc với ...require.requireActualhoặc sau khi loại bỏ răng cưa từ con đường
Tzahi Leh

1
Bạn sẽ kiểm tra nó như thế nào otherFuntrong trường hợp này? Giả sửotherFn: jest.fn()
Stevula

1
@Stevula Tôi đã cập nhật câu trả lời của mình để hiển thị ví dụ sử dụng thực tế. Tôi chỉ ra mockReturnValuephương pháp để chứng minh tốt hơn rằng phiên bản bị chế nhạo đang được gọi thay vì phiên bản gốc, nhưng nếu bạn thực sự chỉ muốn xem liệu nó đã được gọi hay chưa mà không xác nhận với giá trị trả về, bạn có thể sử dụng jest matcher .toHaveBeenCalled().
gfullam

36
import m from '../myModule';

Không hoạt động với tôi, tôi đã sử dụng:

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

m.otherFn = jest.fn();

5
Bạn sẽ khôi phục chức năng ban đầu của otherFn như thế nào sau khi kiểm tra để nó không ảnh hưởng đến các testS khác?
Aequitas

1
Tôi nghĩ bạn có thể cấu hình jest để xóa mocks sau mỗi lần kiểm tra? Từ tài liệu: "Tùy chọn cấu hình clearMocks có sẵn để xóa các chế độ giả tự động giữa các lần kiểm tra.". Bạn có thể đặt clearkMocks: truecấu hình jest package.json. facebook.github.io/jest/docs/en/mock-
Cole

2
Nếu đây là vấn đề như vậy để thay đổi trạng thái toàn cục, bạn luôn có thể lưu trữ chức năng ban đầu bên trong một số loại biến thử nghiệm và khôi phục nó sau khi thử nghiệm
bobu

1
const gốc; beforeAll (() => {original = m.otherFn; m.otherFn = jest.fn ();}) afterAll (() => {m.otherFn = original;}) nó sẽ hoạt động, tuy nhiên tôi đã không kiểm tra it
bobu

Cảm ơn bạn rất nhiều! Điều này đã giải quyết vấn đề của tôi.
Slobodan Krasavčević

25

Có vẻ như tôi đến bữa tiệc này muộn, nhưng vâng, điều này có thể xảy ra.

testFnchỉ cần gọi otherFn bằng mô-đun .

Nếu testFnsử dụng mô-đun để gọi otherFnthì quá trình xuất mô-đun cho otherFncó thể được chế tạo và testFnsẽ gọi mô-đun .


Đây là một ví dụ hoạt động:

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

2
Đây thực chất là phiên bản ES6 của cách tiếp cận được sử dụng tại Facebook và được một nhà phát triển Facebook mô tả ở giữa bài đăng này .
Brian Adams

thay vì nhập myModule vào chính nó, chỉ cần gọiexports.otherFn()
andrhamm

3
@andrhamm exportskhông tồn tại trong ES6. Việc gọi exports.otherFn()hoạt động ngay bây giờ vì ES6 đang được biên dịch theo cú pháp mô-đun trước đó, nhưng nó sẽ bị hỏng khi ES6 được hỗ trợ nguyên bản.
Brian Adams

Đang gặp sự cố chính xác này và tôi chắc chắn rằng mình đã gặp phải sự cố này trước đây. Tôi đã phải loại bỏ một lượng xuất khẩu. <methodname> để giúp cây rung chuyển và nó đã bị hỏng nhiều bài kiểm tra. Tôi sẽ xem liệu điều này có ảnh hưởng gì không, nhưng nó có vẻ rất khó hiểu. Tôi đã gặp vấn đề này một số lần và giống như các câu trả lời khác đã nói, những thứ như babel-plugin-rewire hoặc thậm chí tốt hơn, npmjs.com/package/rewiremock mà tôi khá chắc chắn cũng có thể làm được điều trên.
Astridax

Có thể thay vì chế nhạo một giá trị trả về, hãy chế nhạo một cú ném không? Chỉnh sửa: bạn có thể, đây là cách stackoverflow.com/a/50656680/2548010
Big Money

10

Mã được chuyển vị sẽ không cho phép babel truy xuất ràng buộc otherFn()đang tham chiếu đến. Nếu bạn sử dụng một hàm expession, bạn sẽ có thể đạt được khả năng chế nhạo 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();

Nhưng như @kentcdodds đã đề cập trong nhận xét trước, bạn có thể sẽ không muốn chế nhạo otherFn(). Thay vào đó, chỉ cần viết một thông số kỹ thuật mới otherFn()và mô phỏng bất kỳ lệnh gọi cần thiết nào mà nó đang thực hiện.

Ví dụ: nếu otherFn()đang thực hiện một yêu cầu http ...

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

Tại đây, bạn sẽ muốn mô phỏng http.getvà cập nhật các xác nhận của mình dựa trên các triển khai bị chế nhạo.

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

1
điều gì sẽ xảy ra nếu otherFn và testFn được sử dụng bởi một số mô-đun khác? bạn có cần thiết lập mô-đun http trong tất cả các tệp thử nghiệm sử dụng (tuy nhiên sâu ngăn xếp) 2 mô-đun đó không? Ngoài ra, nếu bạn đã có một bài kiểm tra cho testFn, tại sao không khai thác testFn trực tiếp thay vì http trong các mô-đun sử dụng testFn?
rickmed

1
vì vậy nếu otherFnbị hỏng, sẽ thất bại tất cả các bài kiểm tra phụ thuộc vào đó. Ngoài ra nếu otherFncó 5 if bên trong, bạn có thể cần phải kiểm tra xem của bạn testFncó hoạt động tốt cho tất cả các trường hợp phụ đó không. Bạn sẽ có rất nhiều đường dẫn mã khác để kiểm tra ngay bây giờ.
Totty.js

6

Tôi biết điều này đã được hỏi từ rất lâu trước đây, nhưng tôi chỉ gặp phải tình huống này và cuối cùng đã tìm ra một giải pháp hiệu quả. Vì vậy, tôi nghĩ rằng tôi sẽ chia sẻ ở đây.

Đối với mô-đun:

// myModule.js

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

export function testFn() {
  otherFn();

  // do other things
}

Bạn có thể thay đổi những điều sau:

// myModule.js

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

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

  // do other things
}

xuất chúng dưới dạng hằng số thay vì hàm. Tôi tin rằng vấn đề liên quan đến việc lưu trữ trong JavaScript và việc sử dụng constngăn chặn hành vi đó.

Sau đó, trong thử nghiệm của bạn, bạn có thể có một cái gì đó như sau:

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

Chế độ giả của bạn bây giờ sẽ hoạt động như bạn thường mong đợi.


2

Tôi đã giải quyết vấn đề của mình với sự kết hợp của các câu trả lời mà tôi tìm thấy ở đây:

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

Trên câu trả lời đầu tiên ở đây, bạn cũng có thể sử dụng babel-plugin-rewire để giả lập hàm có tên đã nhập. Bạn có thể kiểm tra phần bề ngoài để tua lại hàm được đặt tên .

Một trong những lợi ích tức thì cho tình huống của bạn ở đây là bạn không cần phải thay đổi cách gọi hàm khác từ hàm của mình.


Làm cách nào để định cấu hình babel-plugin-rewire để hoạt động với node.js?
Timur Gilauri
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.