Làm cách nào để giả định nhập khẩu mô-đun ES6?


141

Tôi có các mô-đun ES6 sau:

mạng.js

export function getDataFromServer() {
  return ...
}

widget.js

import { getDataFromServer } from 'network.js';

export class Widget() {
  constructor() {
    getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }

  render() {
    ...
  }
}

Tôi đang tìm cách để kiểm tra Widget với một ví dụ giả getDataFromServer. Nếu tôi sử dụng các <script>s riêng biệt thay vì các mô-đun ES6, như trong Karma, tôi có thể viết bài kiểm tra của mình như sau:

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(window, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Tuy nhiên, nếu tôi đang kiểm tra các mô-đun ES6 riêng lẻ bên ngoài trình duyệt (như với Mocha + babel), tôi sẽ viết một cái gì đó như:

import { Widget } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(?????) // How to mock?
    .andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Được rồi, nhưng bây giờ getDataFromServerkhông có sẵn window(tốt, hoàn toàn không có window) và tôi không biết cách tiêm trực tiếp vào widget.jsphạm vi của chính mình.

Vậy tôi phải đi đâu từ đây?

  1. Có cách nào để truy cập phạm vi widget.jshoặc ít nhất là thay thế nhập khẩu của nó bằng mã của riêng tôi không?
  2. Nếu không, làm thế nào tôi Widgetcó thể kiểm tra?

Thứ tôi đã xem xét:

a. Hướng dẫn tiêm phụ thuộc.

Xóa tất cả nhập khẩu từ widget.jsvà mong đợi người gọi cung cấp deps.

export class Widget() {
  constructor(deps) {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}

Tôi rất khó chịu với việc làm rối giao diện công khai của Widget như thế này và tiết lộ chi tiết triển khai. Không đi.


b. Cho phép nhập khẩu để cho phép chế nhạo họ.

Cái gì đó như:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

export class Widget() {
  constructor() {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}

sau đó:

import { Widget, deps } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(deps.getDataFromServer)  // !
      .andReturn("mockData");
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Điều này ít xâm lấn hơn nhưng đòi hỏi tôi phải viết rất nhiều bản tóm tắt cho mỗi mô-đun, và vẫn có nguy cơ tôi sử dụng getDataFromServerthay vì deps.getDataFromServermọi lúc. Tôi không yên tâm về điều đó, nhưng đó là ý tưởng tốt nhất của tôi cho đến nay.


Nếu không có hỗ trợ giả bản địa cho loại nhập này, có lẽ tôi sẽ nghĩ đến việc viết một biến áp riêng cho babel chuyển đổi kiểu nhập ES6 của bạn sang hệ thống nhập có thể giả được tùy chỉnh. Điều này chắc chắn sẽ thêm một lớp thất bại có thể khác và thay đổi mã bạn muốn kiểm tra, ....
t.niese

Tôi không thể thiết lập bộ kiểm tra ngay bây giờ, nhưng tôi sẽ thử sử dụng hàm jasmin createSpy( github.com/jasmine/jasmine/blob/iêu ) với một tham chiếu được nhập vào getDataFromServer từ mô-đun 'network.js'. Vì vậy, trong tệp thử nghiệm của tiện ích bạn nhập getDataFromServer, và sau đó sẽlet spy = createSpy('getDataFromServer', getDataFromServer)
Được vi xử lý vào

Dự đoán thứ hai là trả về một đối tượng từ mô đun 'network.js', không phải là một hàm. Theo cách đó, bạn có thể spyOntrên đối tượng đó, được nhập từ network.jsmô-đun. Nó luôn luôn là một tham chiếu đến cùng một đối tượng.
Microfed 6/2/2016

Trên thực tế, nó đã là một đối tượng, từ những gì tôi có thể thấy: babeljs.io/repl/NH
Microfed 6/2/2016

2
Tôi thực sự không hiểu làm thế nào tiêm phụ thuộc làm rối Widgetgiao diện công cộng? Widgetbị rối mà không có deps . Tại sao không làm cho sự phụ thuộc rõ ràng?
kiện

Câu trả lời:


129

Tôi đã bắt đầu sử dụng import * as objkiểu trong các thử nghiệm của mình, nhập khẩu tất cả xuất khẩu từ một mô-đun làm thuộc tính của một đối tượng mà sau đó có thể bị chế giễu. Tôi thấy điều này sẽ sạch hơn rất nhiều so với việc sử dụng một cái gì đó như tua lại hoặc proxy hoặc bất kỳ kỹ thuật tương tự nào. Tôi đã làm điều này thường xuyên nhất khi cần chế giễu các hành động của Redux chẳng hạn. Đây là những gì tôi có thể sử dụng cho ví dụ của bạn ở trên:

import * as network from 'network.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(network, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Nếu chức năng của bạn là xuất khẩu mặc định, thì import * as network from './network'nó sẽ tạo ra {default: getDataFromServer}và bạn có thể giả định network.default.


3
Bạn có sử dụng import * as objduy nhất trong thử nghiệm hoặc cũng trong mã thông thường của bạn?
Châu Thái

36
@carpeliam Điều này sẽ không hoạt động với thông số mô-đun ES6 nơi nhập khẩu chỉ đọc.
tro bụi

7
Jasmine đang phàn nàn [method_name] is not declared writable or has no setterđiều đó có ý nghĩa vì nhập khẩu es6 là không đổi. Có cách nào để giải quyết?
lpan

2
@Francisc import(không giống như require, có thể đi bất cứ đâu) bị treo lên, vì vậy về mặt kỹ thuật bạn không thể nhập nhiều lần. Âm thanh như gián điệp của bạn đang được gọi ở nơi khác? Để giữ cho các bài kiểm tra không bị rối loạn trạng thái (được gọi là kiểm tra ô nhiễm), bạn có thể đặt lại các gián điệp của mình trong một AfterEach (ví dụ: sinon.sandbox). Jasmine tôi tin rằng điều này tự động.
Carpeliam

10
@ agent47 Vấn đề là trong khi thông số ES6 đặc biệt ngăn câu trả lời này hoạt động, chính xác như cách bạn đã đề cập, hầu hết những người viết importtrong JS của họ không thực sự sử dụng các mô-đun ES6. Một cái gì đó như webpack hoặc babel sẽ bước vào thời gian xây dựng và chuyển đổi nó thành cơ chế bên trong của riêng chúng để gọi các phần xa của mã (ví dụ __webpack_require__) hoặc thành một trong các tiêu chuẩn thực tế trước ES6 , CommonJS, AMD hoặc UMD. Và chuyển đổi đó thường không tuân thủ nghiêm ngặt thông số kỹ thuật. Vì vậy, đối với nhiều người, nhiều nhà phát triển, câu trả lời này hoạt động tốt. Để bây giờ.
daemonexmachina

31

@carpeliam đúng nhưng lưu ý rằng nếu bạn muốn theo dõi một chức năng trong một mô-đun và sử dụng một chức năng khác trong mô-đun đó gọi chức năng đó, bạn cần gọi chức năng đó là một phần của không gian tên xuất khẩu nếu không gián điệp sẽ không được sử dụng.

Ví dụ sai:

// mymodule.js

export function myfunc2() {return 2;}
export function myfunc1() {return myfunc2();}

// tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // out will still be 2
    });
});

Ví dụ đúng:

export function myfunc2() {return 2;}
export function myfunc1() {return exports.myfunc2();}

// tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // out will be 3 which is what you expect
    });
});

4
Tôi ước tôi có thể bình chọn câu trả lời này thêm 20 lần nữa! Cảm ơn bạn!
sfletche

Ai đó có thể giải thích tại sao đây là trường hợp? Là export.myfunc2 () là bản sao của myfunc2 () mà không phải là tài liệu tham khảo trực tiếp?
Colin Whitmarsh

2
@ColinWhitmarsh exports.myfunc2là một tham chiếu trực tiếp myfunc2cho đến khi spyOnthay thế nó bằng một tham chiếu đến một chức năng gián điệp. spyOnsẽ thay đổi giá trị của exports.myfunc2và thay thế nó bằng một đối tượng gián điệp, trong khi myfunc2vẫn chưa được xử lý trong phạm vi của mô-đun (vì spyOnkhông có quyền truy cập vào nó)
madprog

không nên nhập với *đóng băng đối tượng và các thuộc tính đối tượng không thể thay đổi?
đặc vụ47

1
Chỉ cần lưu ý rằng khuyến nghị sử dụng này export functioncùng với exports.myfunc2kỹ thuật pha trộn cú pháp chung và cú pháp mô-đun ES6 và điều này không được phép trong các phiên bản mới hơn của gói web (2+) yêu cầu sử dụng cú pháp mô-đun ES6 hoàn toàn hoặc không có gì. Tôi đã thêm một câu trả lời dưới đây dựa trên câu trả lời này sẽ hoạt động trong môi trường nghiêm ngặt ES6.
QuarkleMotion

6

Tôi đã triển khai một thư viện cố gắng giải quyết vấn đề nhạo báng thời gian nhập của lớp Typecript mà không cần lớp gốc để biết về bất kỳ nội dung phụ thuộc rõ ràng nào.

Thư viện sử dụng import * ascú pháp và sau đó thay thế đối tượng được xuất ban đầu bằng một lớp sơ khai. Nó giữ lại sự an toàn của loại để các thử nghiệm của bạn sẽ bị hỏng khi biên dịch nếu tên phương thức đã được cập nhật mà không cập nhật thử nghiệm tương ứng.

Thư viện này có thể được tìm thấy ở đây: ts-mock-nhập khẩu .


1
Mô-đun này cần nhiều sao github hơn
SD

6

Câu trả lời của @ vdloo đã đưa tôi đi đúng hướng, nhưng sử dụng cả hai từ khóa "xuất khẩu" và mô-đun ES6 chung trong cùng một tệp không hoạt động với tôi (webpack v2 hoặc sau đó phàn nàn). Thay vào đó, tôi đang sử dụng một xuất khẩu mặc định (biến có tên) bao bọc tất cả các xuất khẩu mô-đun có tên riêng lẻ và sau đó nhập xuất khẩu mặc định trong tệp thử nghiệm của tôi. Tôi đang sử dụng thiết lập xuất sau đây với mocha / sinon và stubbing hoạt động tốt mà không cần tua lại, v.v.:

// MyModule.js
let MyModule;

export function myfunc2() { return 2; }
export function myfunc1() { return MyModule.myfunc2(); }

export default MyModule = {
  myfunc1,
  myfunc2
}

// tests.js
import MyModule from './MyModule'

describe('MyModule', () => {
  const sandbox = sinon.sandbox.create();
  beforeEach(() => {
    sandbox.stub(MyModule, 'myfunc2').returns(4);
  });
  afterEach(() => {
    sandbox.restore();
  });
  it('myfunc1 is a proxy for myfunc2', () => {
    expect(MyModule.myfunc1()).to.eql(4);
  });
});

Câu trả lời hữu ích, cảm ơn. Chỉ muốn đề cập rằng let MyModulekhông bắt buộc phải sử dụng xuất mặc định (nó có thể là một đối tượng thô). Ngoài ra, phương pháp này không yêu cầu myfunc1()gọi myfunc2(), nó hoạt động để chỉ theo dõi trực tiếp.
Đánh dấu Edington

@QuarkleMotion: Có vẻ như bạn đã chỉnh sửa tài khoản này bằng một tài khoản khác với tài khoản chính của bạn một cách tình cờ. Đó là lý do tại sao bản chỉnh sửa của bạn phải trải qua phê duyệt thủ công - có vẻ như đó là từ bạn. Tôi cho rằng đây chỉ là một tai nạn, nhưng, nếu đó là cố ý, bạn nên đọc chính sách chính thức về các tài khoản con rối để bạn đừng vô tình vi phạm các quy tắc .
Trình biên dịch dễ thấy

1
@ConspicuptCompiler cảm ơn vì đã đề phòng - đây là một sai lầm, tôi không có ý định sửa đổi câu trả lời này với tài khoản SO liên kết email của tôi.
QuarkleMotion

Đây dường như là một câu trả lời cho một câu hỏi khác! Widget.js và network.js ở đâu? Câu trả lời này dường như không có sự phụ thuộc quá độ, đó là điều làm cho câu hỏi ban đầu trở nên khó khăn.
Bennett McElwee

3

Tôi đã tìm thấy cú pháp này để làm việc:

Mô-đun của tôi:

// mymod.js
import shortid from 'shortid';

const myfunc = () => shortid();
export default myfunc;

Mã kiểm tra mô-đun của tôi:

// mymod.test.js
import myfunc from './mymod';
import shortid from 'shortid';

jest.mock('shortid');

describe('mocks shortid', () => {
  it('works', () => {
    shortid.mockImplementation(() => 1);
    expect(myfunc()).toEqual(1);
  });
});

Xem tài liệu .


+1 và với một số hướng dẫn bổ sung: Có vẻ như chỉ hoạt động với các mô-đun nút, tức là những thứ bạn có trên pack.json. Và quan trọng hơn là, một cái gì đó không được đề cập trong các tài liệu Jest, chuỗi được truyền vào jest.mock()phải khớp với tên được sử dụng trong import / packge.json thay vì tên của hằng. Trong các tài liệu chúng đều giống nhau, nhưng với mã như import jwt from 'jsonwebtoken'bạn cần thiết lập giả nhưjest.mock('jsonwebtoken')
kaskelotti

0

Tôi đã không thử nó, nhưng tôi nghĩ rằng sự nhạo báng có thể làm việc. Nó cho phép bạn thay thế mô-đun thực bằng một mô phỏng mà bạn đã cung cấp. Dưới đây là một ví dụ để cung cấp cho bạn ý tưởng về cách thức hoạt động của nó:

mockery.enable();
var networkMock = {
    getDataFromServer: function () { /* your mock code */ }
};
mockery.registerMock('network.js', networkMock);

import { Widget } from 'widget.js';
// This widget will have imported the `networkMock` instead of the real 'network.js'

mockery.deregisterMock('network.js');
mockery.disable();

Có vẻ như mockerynó không còn được duy trì nữa và tôi nghĩ rằng nó chỉ hoạt động với Node.js, nhưng không kém, đó là một giải pháp gọn gàng cho việc mô phỏng các mô-đun khó chế giễu.

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.