Master Jasmine Spies: Isolate Your Unit Tests with spyOn and createSpy
Jasmine spies let you replace real collaborators with lightweight stand‑ins. This guide shows how to use spyOn, createSpy, and createSpyObj, gives a concrete example, discusses trade‑offs, and offers actionable best‑practice tips.
02 Oct 2025, 11:51 UTC

The Problem: Dependencies Make Unit Tests Fragile
When a unit under test calls out to a database, HTTP client, or timer, the test must decide whether to use the real collaborator or a stand‑in. Using the real one makes the test slow and flaky; stubbing it too aggressively can hide bugs that only surface in integration.
Jasmine’s spy utilities give you a middle ground: replace a method with a lightweight spy that records every call, lets you assert on interactions, and optionally controls the method’s return value.
Thesis: Jasmine Spies Keep Tests Fast, Realistic, and Verifiable
By leveraging spyOn, jasmine.createSpy, and jasmine.createSpyObj, you can isolate the unit’s behavior, verify that it talks to its collaborators correctly, and avoid the overhead of a full mocking framework.
Section 1 – spyOn: Replace an Existing Method
spyOn(obj, 'method') temporarily replaces obj.method with a spy. The spy records every call, arguments, and invocation count. After each spec, Jasmine automatically restores the original method, so no manual cleanup is needed unless you modify the prototype outside the spec.
// Example: spy on a console method
const consoleObj = console;
spyOn(consoleObj, 'log');
consoleObj.log('hello');
expect(consoleObj.log).toHaveBeenCalledWith('hello');
Key points:
- Property must exist at spy time; otherwise Jasmine throws an error (especially in newer versions).
- Spies can be configured with
.andmethods (see next section). - Spies are automatically cleaned up between specs.
Section 2 – jasmine.createSpy & jasmine.createSpyObj: Build Stand‑Ins
jasmine.createSpy('name') creates a standalone spy function. It’s useful for injecting callbacks or simple dependencies.
const fakeCallback = jasmine.createSpy('fakeCallback');
myFunction(fakeCallback);
expect(fakeCallback).toHaveBeenCalled();
For objects with multiple methods, jasmine.createSpyObj('name', ['m1','m2']) returns an object where each listed method is a spy. This reduces boilerplate when you need a mock service.
const httpSpy = jasmine.createSpyObj('HttpClient', ['get', 'post']);
httpSpy.get.and.returnValue(of({data: 42}));
Section 3 – Controlling Spy Behavior
After creating a spy, you can decide how it behaves:
.and.returnValue(value)– always returnvalue..and.callThrough()– invoke the original implementation. Useful when you want to spy on a method but still run its real logic..and.callFake(fn)– replace the method withfn. You can implement custom logic..and.throwError(error)– make the spy throw.
Example: Stub an HTTP call but keep error handling logic intact.
spyOn(httpClient, 'get').and.callFake((url) => {
if (url === '/bad') {
throw new Error('404');
}
return of({id: 1});
});
Section 4 – Asserting Interactions
Jasmine provides matchers that let you verify how a spy was used:
toHaveBeenCalled()– at least once.toHaveBeenCalledTimes(n)– exactlyntimes.toHaveBeenCalledWith(...args)– called with specific arguments.toHaveBeenCalledBefore(otherSpy)– ensures call order.
These assertions focus on interaction, not state, keeping your tests decoupled from implementation details.
Section 5 – Practical Example: Testing a Service that Calls HttpClient
Suppose we have a UserService that fetches user data via Angular’s HttpClient:
export class UserService {
constructor(private http: HttpClient) {}
getUser(id: number) {
return this.http.get(`/api/users/${id}`);
}
}
We want to test getUser without making an actual HTTP request. Using spies we can:
- Inject a spy object for
HttpClient. - Stub
getto return a fake observable. - Assert that
getwas called with the correct URL.
describe('UserService', () => {
let service: UserService;
let httpSpy: jasmine.SpyObj<HttpClient>;
beforeEach(() => {
httpSpy = jasmine.createSpyObj('HttpClient', ['get']);
service = new UserService(httpSpy as any);
});
it('should fetch user by id', () => {
const fakeUser = { id: 5, name: 'Alice' };
httpSpy.get.and.returnValue(of(fakeUser));
service.getUser(5).subscribe(user => {
expect(user).toEqual(fakeUser);
});
expect(httpSpy.get).toHaveBeenCalledWith('/api/users/5');
expect(httpSpy.get).toHaveBeenCalledTimes(1);
});
});
Notice:
- The test remains fast because no real HTTP call occurs.
- The spy records the URL, so if the service’s URL construction changes, the test will fail.
- We still exercise the real
UserService.getUserlogic.
Trade‑Off: Over‑Stubbing Can Hide Integration Issues
When every dependency is spied on, the test verifies only that the unit calls its collaborators. It does not catch bugs that arise from the real implementation of those collaborators. For example, if HttpClient.get suddenly throws a network error, the spy will still return the fake user and the test will pass.
To mitigate this, keep a small suite of integration tests that exercise the unit with real collaborators. Use spies for unit tests that need speed and determinism, and integration tests for end‑to‑end validation.
Actionable Closing – Best‑Practice Checklist
- Use
spyOnfor existing methods you want to observe or stub. - Use
jasmine.createSpyObjwhen you need a whole mock object with multiple methods. - Configure spies with
.and.returnValueor.and.callFaketo return realistic data. - Always assert on interactions with
toHaveBeenCalledWithto guard against signature changes. - Verify the Jasmine version (e.g.,
npm ls jasmine) and consult the matching API docs, as matcher names and behavior differ between 2.x and 3.x/4.x/5.x. - Run a quick sanity check: change the real method’s signature and confirm that stubs no longer match the expected arguments.
- Complement spy‑heavy unit tests with a few integration tests that use real collaborators.
By following these patterns, you keep your unit tests fast, maintainable, and reliable, while still catching the bugs that matter.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.