Tìm kiếm trang web

typescript - Gián điệp một thuộc tính/hàm của biến riêng tư với Jasmine


Tôi có một hàm với chức năng biến đổi dựa trên tệp mà nó đọc, được điều khiển thông qua một Map mà nó lưu trong bộ nhớ:

file1.ts
function f1(x: number): number {
  // do far-reaching things
  return 1;
}

function f2(x: number): number {
  // do different far-reaching things
  return 2;
}

function f3(x: number): number {
  // do still-different far-reaching things
  return 3;
}

const myMap: Map<string, (number) => number> = new Map<string, () => void>([
  ['key1', f1],
  ['key2', f2],
  ['key3', f3],
]

export function doThing(filename: string): number {
  // open file, make some database calls, and figure out the name of a key
  // ...
  let fileToExecute = myMap.get(key);
  return fileToExecute(someValueDerivedFromFile);
}

f1, f2 và f3 đều thực hiện nhiều hơn những gì được hiển thị ở đây, và mỗi cái đều cần rất nhiều giả lập để được kiểm tra thành công.

Khi mã trở nên phát triển hơn và các trường hợp sử dụng tiếp tục tăng, sẽ có một số lượng tùy ý các hàm có thể cần được gọi, dựa trên một tập hợp các đầu vào ngày càng mở rộng. doThing() phức tạp và lấy thông tin từ nhiều nguồn khác nhau, bao gồm cả nội dung của tệp được cung cấp và cơ sở dữ liệu, điều này giúp nó chọn một tệp để thực thi. Từ góc nhìn của khách hàng, doThing() là hàm duy nhất mà họ quan tâm. Do đó, nó là hàm duy nhất được export bởi tệp này.

Tôi đang cố gắng kiểm tra cơ chế trong doThing() để xác định key mà nó nên sử dụng. Tôi không muốn giả lập f1, f2, và f3 cụ thể - tôi muốn đưa ra nhiều tùy chọn hơn, được trỏ đến bởi các thứ khác mà tôi đang giả lập cho doThing(). Tuy nhiên, để kiểm tra xem nó có gọi phương thức giả đúng hay không, tôi cần xác định phương thức giả nào mà nó đang gọi. Giải pháp tôi thử dùng là ép kiểu để cố lấy myMap riêng tư ra khỏi tệp và sau đó theo dõi phương thức get() của nó:

file1.spec.ts
import * as file1 from '../src/file1'
...
it("calls the correct fake method", () => {
  // lots of other mocks
  let spies = [
    jasmine.createSpy('f1spy').and.returnValue(4),
    jasmine.createSpy('f2spy').and.returnValue(5),
    jasmine.createSpy('f3spy').and.returnValue(6),
    ...
  ]
  let mockMap = spyOn((file1 as any).myMap, 'get').and.callFake((key) => {  // this fails
    var spy;
    switch(key) {
      case 'key1': spy = spies[0]; break;
      case 'key2': spy = spies[1]; break;
      case 'key3': spy = spies[2]; break;
      ...
    }
    return spy;
  }

  result = file1.doThing(...);

  expect(spies[0]).not.toHaveBeenCalled();
  expect(spies[1]).toHaveBeenCalledWith(7);
  expect(spies[2]).not.toHaveBeenCalled();
});

Tuy nhiên, tôi nhận được lỗi ở dòng có chú thích phía trên: Error: <spyOn> : không thể tìm thấy đối tượng để theo dõi cho get(). Sau khi điều tra thêm (tức là sử dụng trình gỡ lỗi từng bước), hóa ra đối tượng file1 mà tôi đã nhập chỉ có doThing(), và không có bất kỳ biến riêng nào khác của nó.

Làm thế nào để tôi giả lập thành công việc biến đổi khóa-giá trị ở đây - điều này có nghĩa là, trong trường hợp này, theo dõi các thuộc tính của một biến riêng tư, để tôi có thể đặt các spy của mình vào đúng vị trí? Hoặc thay thế hoàn toàn myMap hoặc thay thế myMap.get() đều là một lựa chọn, nếu bất kỳ cái nào khả thi.

Câu trả lời #1

  1. Jasmine, theo những gì tôi biết, không sử dụng bất kỳ phép thuật kiểu biên dịch nào, vì vậy Jasmine không thể truy cập vào các biến riêng tư của bạn.

  2. Từ quan điểm của khách hàng, doThing() là chức năng duy nhất mà họ quan tâm. Do đó, nó là chức năng duy nhất được xuất khẩu bởi tệp này.

    nhưng điều này không có nghĩa là bạn nên tước quyền truy cập của các bài kiểm tra của bạn đối với nhân sự dư thừa. Thay vào đó, bạn có thể tạo hai tệp

    file1.ts - cho một khách hàng

    import { doThing } from "./file1_implementation"
    export doThing
    

    và file1_implementation.ts - cho các bài kiểm tra của bạn

    export function f1(...) ...
    export function f2(...) ...
    export function f3(...) ...
    export const myMap ...
    export function doThing(...) ...
    

    sau đó trong file1.spec.ts bạn có thể sử dụng file1_implementation.ts và bạn sẽ có quyền truy cập vào mọi thứ bạn cần

    import * as file1 from '../src/file1_implementation'
    ...
    

Câu trả lời #2

Ý tưởng chung: sử dụng rewire.

Sử dụng rewire, chúng tôi sẽ ghi đè các hàm riêng tư của bạn bằng các hàm spy.

Tuy nhiên, const myMap của bạn cần được sửa đổi. Bởi vì khi bạn làm ['key1', f1] - nó sẽ lưu trữ triển khai hiện tại của f1, nên chúng ta không thể ghi đè nó sau khi myMap đã được khởi tạo. Một trong những cách để khắc phục điều này là sử dụng ['key1', args => f1(args)]. Bằng cách này, nó sẽ không lưu trữ hàm f1, chỉ lưu trình bao để gọi nó. Bạn cũng có thể đạt được điều tương tự bằng cách sử dụng apply() hoặc call().

Ví dụ về triển khai:

file1.ts:

function f1(): number {
  // do far-reaching things
  return 1;
}

const myMap: Map<string, (x: number) => number> = new Map([
  ['key1', (...args: Parameters<typeof f1>) => f1(...args)],
]);

export function doThing(): number {
  const key = 'key1';
  const magicNumber = 7;
  const fileToExecute = myMap.get(key);
  return fileToExecute(magicNumber);
}

file1.spec.ts:

import * as rewire from 'rewire';

it('calls the correct fake method', () => {
  const spies = [jasmine.createSpy('f1spy').and.returnValue(4)];

  const myModule = rewire('./file1');

  myModule.__set__('f1', spies[0]);

  myModule.doThing();

  expect(spies[0]).toHaveBeenCalledWith(7);
});

Để sử dụng rewire với TypeScript, bạn có thể muốn sử dụng babel, v.v.

Để chứng minh ý tưởng, tôi chỉ định sẽ biên dịch nó:

./node_modules/.bin/tsc rewire-example/*

và chạy các bài kiểm tra:

./node_modules/.bin/jasmine rewire-example/file1.spec.js

Cái nào sẽ chạy thành công:

Started
.


1 spec, 0 failures

CẬP NHẬT

Không có sửa đổi đối với myMap:

file1.spec.ts:

import * as rewire from 'rewire';

it('calls the correct fake method', () => {
  const spies = [
    jasmine.createSpy('f1spy').and.returnValue(4),
    jasmine.createSpy('f2spy').and.returnValue(5),
    // ...
  ];

  const myModule = rewire('./file1');

  const myMockedMap: Map<string, (x: number) => number> = new Map();

  (myModule.__get__('myMap') as typeof myMockedMap).forEach((value, key) =>
    myMockedMap.set(key, value)
  );

  myModule.__set__('myMap', myMockedMap);

  // ...
});

Bài viết liên quan