Следите за атрибутом / функцией приватной переменной с помощью Jasmine - PullRequest
6 голосов
/ 04 апреля 2020

У меня есть функция с переменным функционалом, основанная на файле, который она читает, который управляется через Map, который он хранит в памяти:

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 и f3 все делают намного больше, чем показано здесь, и каждый требует большого количества макетов для успешного тестирования.

По мере того, как код становится все более развитым и варианты использования продолжайте, будет произвольное количество функций, которые, возможно, потребуется вызвать, основываясь на расширяющемся наборе входных данных. doThing() является сложным и берет информацию из множества различных источников, включая как содержимое данного файла, так и базу данных, что помогает ему выбрать, какой файл выполнять. С точки зрения клиента, doThing() - единственная функция, о которой он заботится. Таким образом, это единственный файл export, редактируемый этим файлом.

Я пытаюсь протестировать механизм в doThing(), который определяет, какой key он должен использовать. Я не хочу издеваться над f1, f2 и f3 конкретно - я хочу представить еще много опций, на которые указывают другие вещи, над которыми я издеваюсь doThing(). Однако, чтобы проверить, вызывает ли он правильный фальшивый метод, мне нужно выяснить, какой фальшивый метод он вызывает. Мое попытанное решение использует приведение типов, чтобы вытащить приватное myMap из файла, а затем шпионить за его get() методом:

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

Однако в аннотированной строке выше появляется сообщение об ошибке: Error: <spyOn> : could not find an object to spy upon for get(). После дальнейшего исследования (то есть пошагового отладчика) выясняется, что импортированный мной file1 объект имеет только doThing() и не имеет никаких других своих частных переменных.

Как мне успешно смоделировать преобразование ключ-значение здесь - что означает, в данном случае, слежку за атрибутами частной переменной, чтобы я мог получить своих шпионов в нужном месте? Можно либо полностью заменить myMap, либо заменить myMap.get(), если это возможно.

Ответы [ 3 ]

4 голосов
/ 10 апреля 2020
  1. Жасмин , насколько я знаю, не использует магию компилятора c, поэтому для Жасмин невозможно получить доступ для вас приватные переменные.

  2. С точки зрения клиента, doThing() - единственная функция, о которой он заботится. Таким образом, это единственный файл, экспортируемый этим файлом.

    , но это не означает, что вы должны лишить свои тесты доступа к избыточному персоналу. Вместо этого вы можете создать два файла

    file1.ts - для клиента

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

    и file1_implementation.ts - для ваших тестов

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

    , затем в file1.spec.ts Вы можете использовать file1_implementation.ts, и у вас будет доступ ко всему, что вам нужно

    import * as file1 from '../src/file1_implementation'
    ...
    
3 голосов
/ 12 апреля 2020

Общая идея: используйте rewire.

Используя rewire, мы переопределим ваши личные функции с spy функциями.

Однако ваш const myMap необходимо изменить. Потому что когда вы делаете ['key1', f1] - он хранит текущую реализацию f1, поэтому мы не можем переопределить его после инициализации myMap. Один из способов преодолеть это - использовать ['key1', args => f1(args)]. Таким образом, он не будет хранить функцию f1, а только обертку для ее вызова. Вы можете достичь того же, используя apply() или call().

Пример реализации:

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

Для использования rewire с машинописью Вы можете использовать babel, et c.

. Для подтверждения концепции я просто скомпилирую:

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

и запустим тесты:

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

, который будет успешно работать:

Started
.


1 spec, 0 failures

ОБНОВЛЕНИЕ

Без изменений 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);

  // ...
});
2 голосов
/ 11 апреля 2020

Можете ли вы просто превратить file1 в класс? Тогда вы можете определенно получить доступ к его закрытым методам / атрибутам из jasmine.

, так что file1 становится:

export class FileHelper {

  private f1 () : void {}
  private f2 () : void {}
  private f3 () : void {}

  private myMap: Map<whatever, whatever>;

  public doThing () : void {}

}

, затем в вашей спецификации c:

let mapSpy: jasmine.Spy;
let myFileHelper: FileHelper;

beforeEach(() => {
  myFileHelper = new FileHelper();
  mapSpy = spyOn(<any>myFileHelper, 'myMap').and.callFake(() => {
    //whatever you were doing
  });
});


it('should do whatever', () => {

});
...