1

Is there a way how to spy on constructor using Jasmine and Typescript?

Proposed solutions in question Spying on a constructor using Jasmine do not work because TypeScript does not expose global declarations on window.

Here is my use case:

// higher-order function returns validation function with max length
const maxLengthValidator = maxLength => value => value.length > maxLength;

class Book {
  // will be a different function instance each time the Book object is
  // created causing the following test to fail
  nameValidator = maxLengthValidator(42);
  name = null;
  constructor (name) { this.name = name; }
}

class Library {
  persons = [];
  storeBook (book) { this.persons.push(book); }
  createBook (name) { this.storeBook(new Book(name)); }
}

Failing test:

it('createBook passes new book with provided name to storeBook', () => {
  const lib = new Library();
  spyOn(lib, 'storeBook');
  lib.createBook('Start with Why');
  expect(lib.storeBook).toHaveBeenCalledWith(new Book('Start with Why'));
  // Result: Expected spy storeBook to have been called with
  // [ Book({ name: 'Start with Why', nameValidator: Function }) ]
  // but actual calls were
  // [ Book({ name: 'Start with Why', nameValidator: Function }) ]
});

In the test, I don't really care what constructor does. I only need to verify that it has been called with the right parameter. That even sounds like a right use case for a mock.

Radek Matěj
  • 569
  • 6
  • 17
  • 3
    You're spying on it correctly. However the test is failing because it is called with a different `Book` instance, even if their "values" are the same. – James Monger Mar 13 '18 at 14:06
  • But Jasmine test passed even for a different `Book` instance until I used the higher-order function. It seems to compare values even if it is a different instance. Is there a way how to write the test with a single `Book` instance? – Radek Matěj Mar 14 '18 at 19:47

1 Answers1

0

Two functions can be compared using their toString value. You can either add a custom equality tester or add new matcher.

Adding custom equality tester

function replacer(k, v) {
    if (typeof v === 'function') {
        v = v.toString();
    }
    return v;
}

function stringifyEquality(a, b) {
    try {
        return JSON.stringify(a, replacer) === JSON.stringify(b, replacer) ? true : undefined;
    } catch (e) {
        return undefined
    }
}

beforeAll(() => {
    jasmine.addCustomEqualityTester(stringifyEquality);
});

it('createBook passes new book with provided name to storeBook', () => {
    const lib = new Library();
    spyOn(lib, 'storeBook');
    lib.createBook('Start with Why');
    expect(lib.storeBook.calls.mostRecent().args).toEqual([new Book('Start with Why')]);
});

Adding custom matcher

function replacer(k, v) {
    if (typeof v === 'function') {
        v = v.toString();
    }
    return v;
}

beforeEach(() => {
    jasmine.addMatchers({
        toBeJsonEqual: function(expected){
            var one = JSON.stringify(this.actual, replacer).replace(/(\\t|\\n)/g,''),
                two = JSON.stringify(expected, replacer).replace(/(\\t|\\n)/g,'');

                return one === two;
            }
    });
});

expect(obj).toBeJsonEqual(obj2);

links: question 14541287 (pocesar's answer)