Using Jasmine spyOn to Verify Calls Without Coupling Tests to Implementation
Learn how Jasmine's spyOn lets you verify function calls and arguments without running real implementations, plus the trade‑offs that keep tests maintainable.
25 Mar 2026, 07:25 UTC

The problem: verifying a call without executing the real code
When a unit test needs to confirm that a dependency was invoked with specific arguments, running the actual implementation can introduce network traffic, timers, or database writes. Jasmine’s spyOn solves this by swapping the target method with a spy that records every invocation while optionally stubbing its behavior.
How spyOn works
spyOn(object, 'methodName') replaces the named function on object with a spy. The spy records:
- whether it was called (
callCount) - the arguments for each call (
argsForCall(index),mostRecentCall.args,allArgs()) - the return value of each call
By default the spy delegates to the original implementation via and.callThrough(), so existing behavior remains unless you explicitly change it.
Configuring spy behavior
Chained modifiers let you control what the spy does when invoked:
and.returnValue(value)– returns a fixed value.and.callFake(fn)– runs a custom function instead.and.stub()– does nothing and returnsundefined.and.callThrough()– executes the real method (default).
These modifiers can be combined in a single statement, e.g. spyOn(api, 'get').and.callFake(() => Promise.resolve({data: 42})).
Inspecting recorded calls
After the code under test runs, the spy exposes a small inspection API:
expect(spy.callCount).toBe(2);
expect(spy.argsForCall(0)).toEqual(['/users/1']);
expect(spy.mostRecentCall.args).toEqual(['/users/2']);
expect(spy.allArgs()).toEqual([['/users/1'], ['/users/2']]);
These matchers let you assert exact argument shapes without caring about the implementation behind the spy.
Worked example: spying on an HTTP client
Assume a module userService.js that fetches a user profile:
// userService.js
const http = require('./httpClient');
function fetchUser(id) {
return http.get(`/users/${id}`);
}
module.exports = { fetchUser };
The test file creates a spy on http.get, configures a fake response, and verifies the call:
// userService.spec.js
const http = require('./httpClient');
const { fetchUser } = require('./userService');
describe('fetchUser', () => {
let getSpy;
beforeEach(() => {
// Replace http.get with a spy that returns a resolved promise
getSpy = spyOn(http, 'get').and.callFake(() => Promise.resolve({ id: 1, name: 'Ada' }));
});
it('calls http.get with the correct path', async () => {
const user = await fetchUser(1);
expect(getSpy.callCount).toBe(1);
expect(getSpy.argsForCall(0)[0]).toBe('/users/1');
expect(user.name).toBe('Ada');
});
});
Running the suite with npx jasmine from the project root (requires Node ≥ 14 and Jasmine ≥ 4.x installed as a dev dependency) will execute the spec. The spy records the call, the fake implementation supplies a deterministic response, and the assertions confirm both the argument and the returned value.
Trade‑offs and limitations
- Coupling to implementation: Asserting the exact call sequence (e.g.,
expect(spy).toHaveBeenCalledBefore(otherSpy)) makes the test brittle when the internal algorithm changes but the observable result stays the same. - Spy leakage: If the same object is shared across specs and the spy is not restored, later tests see the spy instead of the real method. Jasmine restores spies automatically when they are created in a
beforeEach, but manual spies on prototypes or module‑level singletons need explicitspy.calls.reset()orspy.and.callThrough()cleanup. - Missing method error:
spyOnthrows at runtime if the target object lacks the named property. For purely synthetic callbacks usejasmine.createSpy('callback')instead. - Side effects with
callThrough: The default delegation still runs the real code. If the real method performs I/O, you must override withand.stub()orand.callFake()to keep the test fast and isolated. - Version differences: Jasmine 3.x introduced the current chaining API; older 2.x releases used
andReturnandandCallFake. Verify the installed major version inpackage.jsonbefore adopting newer syntax.
Actionable checklist for responsible spy usage
- Prefer spies for external boundaries (HTTP, timers, filesystem) rather than internal helper functions.
- Stub the behavior (
and.returnValue,and.callFake) instead of relying oncallThroughunless you explicitly need the real side effect. - Assert on what was called (arguments, count) not how many times a private method was invoked.
- Create spies in
beforeEachso Jasmine restores them automatically; avoid attaching spies to shared prototypes. - Run a quick sanity spec that calls the spied method twice with different arguments and logs
spy.argsForCall(0)andspy.argsForCall(1)to confirm argument tracking works in your environment.
Following these guidelines keeps tests focused on observable behavior while still giving you the verification power that spyOn provides.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.