When does Jasmine .toHaveBeenCalledWith match the parameters?

Harsh Shah picture Harsh Shah · Feb 4, 2018 · Viewed 10.6k times · Source

I have the following angular service and its jasmine test. The test calls f1() and spies on f2(). The function f2 takes variable v2 and modifies it (sets field a to 3). The function f2 should be called with v2 (as declared in f1) but my test fails on toHaveBeenCalledWith and says actual call was with the object as after the f2 function call. Does jasmine match the parameters for .toHaveBeenCalledWith after the function call, which shouldn't be the recommended way or am I making some mistake here.

Service:

export class JasmineTestClass{
    constructor(){
    }
    f2(v2){
        v2.a = 3
    };
    f1(v1){
        let v2 = {
            a:30
        };
        this.f2(v2);
    }
}

Test:

describe('Test', () => {
    let service: JasmineTestClass;
    beforeEach(() => {
        service = new JasmineTestClass();
        spyOn(service, 'f2').and.callThrough();
    });
    let v1 = {
        a:2, b:3
    };
    let v2 = {
        a:30
    };
    it('should succeed', () => {
        service.f1(v1);
        expect(service.f2).toHaveBeenCalledWith(v2);    //this is failing
    });
})

Log:

Test should succeed FAILED

Expected spy f2 to have been called with [Object ({a:30})] but actual calls were [Object ({a:3})]

Please note that I did debug in Chrome while testing and the function f2() is called with v2 = {a:30}.

Answer

Estus Flask picture Estus Flask · Feb 4, 2018

toHaveBeenCalledWith matches call arguments when it's asserted.

Jasmine spies just save argument references internally. Call arguments can be traced by logging service.f2.calls.all() object.

The problem here is that f2 modifies the object that was passed by reference to it. Original v2 with v2.a === 30 doesn't exist anywhere after f2 call.

The proper strategy for such cases is to create fine-grained tests, one test per unit (method). The fact that callThrough is used indicates that units aren't isolated from each other, it's a good idea to use just spyOn(service, 'f2') stub by default:

it('should succeed', () => {
  service.f1(v1);
  expect(service.f2).toHaveBeenCalledWith({ a:30 });
});

it('should succeed', () => {
  const obj = { a:30 };
  service.f2(obj);
  expect(obj).toEqual({ a:3 });
});

Now we're testing what exactly is going on in these two methods.