Spy on an attribute/function of a private variable with Jasmine

  1. Jasmine, as far as I know, doesn't use any compiler-kind magic, so it's impossible for Jasmine to get access to you private variables.

  2. From a client's point of view, doThing() is the only function it cares about. Thus, it's the only one exported by this file.

    but this doesn't mean that you should deprive your tests from the access to over staff. Instead you can create two files

    file1.ts - for a client

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

    and file1_implementation.ts - for your tests

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

    then in file1.spec.ts you can use file1_implementation.ts and you'll have access to everything you need

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

General idea: use rewire.

Using rewire, we will override your private functions with spy functions.

However, your const myMap needs to be modified. Because when you do ['key1', f1] - it stores current implementation of f1, so we can't override it after myMap was initialized. One of the ways to overcome this - use ['key1', args => f1(args)]. This way, it will not store f1 function, only the wrapper to call it. You might achieve the same by using apply() or call().

Example implementation:

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

In order to use rewire with typescript, you might want to use babel, etc.

For proof of concept, I'm just going to compile it:

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

and run tests:

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

Which will run successfully:

Started
.


1 spec, 0 failures

UPDATE

Without modifications to 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);

  // ...
});

Can you just make file1 into a class? Then you can definitely access its private methods / attributes from jasmine.

so file1 becomes:

export class FileHelper {

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

  private myMap: Map<whatever, whatever>;

  public doThing () : void {}

}

then in your spec:

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', () => {

});