Using Jasmine Spies to Isolate Units Without Extra Mocking Libraries
Learn how Jasmine’s native spies let you replace collaborators, track calls, and stub behaviour without adding a mocking library, plus pitfalls to avoid.
14 May 2026, 03:06 UTC

Problem: Tests that depend on real collaborators become brittle
When a unit under test calls a service, makes an HTTP request, or touches the file system, the test suite can fail for reasons unrelated to the unit’s logic. Setting up fakes or mocks often pulls in additional libraries, increasing bundle size and learning cost.
Thesis: Jasmine’s built‑in spy system provides call tracking and stubbing out of the box, letting you replace collaborators with lightweight fakes while keeping the test focused on observable behavior.
How spies work
spyOn(object, 'methodName') replaces an existing method with a spy that records every invocation. If the method does not exist or is non‑configurable, the call throws an error, which surfaces a design‑time issue early.
jasmine.createSpy('name') creates a standalone fake function that can be passed as a dependency.
All spies expose a calls object with helpers:
count()– number of times the spy was invokedargsFor(index)– arguments supplied to a specific callmostRecent()– the latest argument array
Stubbing strategies
After creating a spy you can chain stubbing behaviours:
spyOn(service, 'fetchData')
.and.returnValue(Promise.resolve({ id: 42 }));
Other useful stubs:
and.callFake(fn)– execute a custom function while still tracking callsand.throwError(e)– simulate failure pathsand.callThrough()– delegate to the original implementation while still recording the calland.stub()– reset the spy to a no‑op after it has been configured otherwise
Worked example: testing a presenter that depends on a data service
Consider a simple presenter that loads a user profile and updates the view:
class ProfilePresenter {
constructor(view, service) {
this.view = view;
this.service = service;
}
load() {
this.service.getUser(123).then(user => {
this.view.render(user);
});
}
}
In the spec we replace service.getUser with a spy, stub a resolved value, and assert that the view received the expected data:
describe('ProfilePresenter', () => {
let view, service, presenter;
beforeEach(() => {
view = jasmine.createSpyObj('view', ['render']);
service = jasmine.createSpyObj('service', ['getUser']);
presenter = new ProfilePresenter(view, service);
});
it('renders the user returned by the service', async () => {
const fakeUser = { id: 123, name: 'Ada' };
service.getUser.and.returnValue(Promise.resolve(fakeUser));
presenter.load();
await Promise.resolve(); // wait for the micro‑task
expect(view.render).toHaveBeenCalledTimes(1);
expect(view.render.calls.argsFor(0)[0]).toEqual(fakeUser);
});
it('handles service errors gracefully', () => {
service.getUser.and.throwError(new Error('network'));
presenter.load();
// assuming the presenter catches and logs the error
expect(console.error).toHaveBeenCalledWith(jasmine.any(Error));
});
});
Notice that no external mocking framework is required; the spy records both the call count and the arguments, enabling precise interaction assertions.
Trade‑off and limitation
Over‑specifying interaction details (exact call counts, argument order) couples tests to the implementation of the collaborator. If the presenter later decides to batch requests or debounce calls, those tests will fail even though the observable outcome—what the view displays—remains correct. A healthier approach is to assert on the observable effect (e.g., the view’s final state) and use spies only to verify that a collaborator was invoked, not how many times.
Another limitation: spyOn requires the target property to exist and be configurable on the object. When working with ES modules that export frozen objects or with bundlers that strip properties, the spy will fail. In such cases, pass the collaborator as a constructor argument or setter so the test can replace it with a standalone spy created via jasmine.createSpy.
Actionable closing
Start by auditing your test suite for places where you manually create fakes or pull in a mocking library. Replace those with Jasmine spies:
- Identify the collaborator and ensure it is injected or accessible as a property.
- Use
spyOnif the property exists, otherwisejasmine.createSpy. - Stub the needed return value or error with
and.returnValue/and.throwError. - Assert on the unit’s output and, if necessary, on
spy.calls.count()orspy.calls.argsFor(0). - Let Jasmine’s automatic per‑spec tear‑down clean up the spy.
Run a quick sanity check: after a spec that reconfigures a spy with and.stub(), run a following spec and confirm the spy behaves as a fresh no‑op. This validates that leaked stubs are not contaminating later tests—a common source of order‑dependent failures.
By leaning on Jasmine’s built‑in spy system you keep test setup terse, avoid extra dependencies, and retain the ability to verify both what a unit produces and how it interacts with its collaborators—without over‑constraining the implementation.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.