Using Jasmine spyOn to Mock Dependencies and Verify Interactions in Unit Tests
Learn how Jasmine's spyOn lets you replace real dependencies with test doubles, simulate return values or custom logic, and verify calls without executing production code.
24 Oct 2025, 12:13 UTC

The Problem: Unit Tests That Depend on Real Collaborators
When a function you are testing calls another object—such as a service that makes HTTP requests, reads the file system, or accesses the current time—your test can become slow, flaky, or tightly coupled to the collaborator's behavior. If the real collaborator fails or returns unexpected data, the test fails even though the unit's logic is correct.
Isolating Collaborators with Jasmine spyOn
Jasmine provides spyOn(object, 'methodName') to replace a method on an existing object with a test double. By default the spy records calls but does not execute the original implementation. You can then define what the spy should do using the .and API.
Returning a Fixed Value
Use .and.returnValue(value) when the collaborator should give a predictable result, like a configuration flag or a successful response.
Providing Custom Logic
When you need to simulate asynchronous work, conditional branching, or side effects, .and.callFake(fn) lets you supply a temporary implementation. This is handy for mocking timers, promises, or error conditions.
Worked Example: Testing a UserProfileService that Depends on an ApiClient
Imagine a UserProfileService that calls ApiClient.getUser(id) to fetch data and then formats the response. We want to verify that the service correctly transforms the payload without hitting a real endpoint.
// Collaborator (simplified)
const ApiClient = {
getUser: (id) => {
// In reality this would perform an HTTP request
return fetch(`/api/users/${id}`).then(res => res.json());
}
};
// Unit under test
const UserProfileService = {
async getDisplayName(id) {
const user = await ApiClient.getUser(id);
return `${user.firstName} ${user.lastName}`;
}
};
// Jasmine test suite
describe('UserProfileService.getDisplayName', () => {
it('formats the name from the API response', async () => {
// Arrange: spy on the method and make it return a resolved promise
spyOn(ApiClient, 'getUser').and.returnValue(Promise.resolve({
firstName: 'Ada',
lastName: 'Lovelace'
}));
// Act
const displayName = await UserProfileService.getDisplayName(42);
// Assert: verify the collaborator was called with the expected argument
expect(ApiClient.getUser).toHaveBeenCalledWith(42);
// And verify the service's output
expect(displayName).toBe('Ada Lovelace');
});
});
Implementation Details and Risks
To run the example, ensure Jasmine is installed (npm install --save-dev jasmine) and that the test file is executed by the Jasmine runner. The spyOn call must reference the exact object and method name as a string; otherwise Jasmine throws an error.
Trade‑off: Implementation Coupling
Spying on every internal call makes tests fragile. If you rename a private method or change the internal call order, the test will fail even though the outward behavior is unchanged. Limit spies to the boundaries of the unit—the collaborators it directly depends on.
Limitations to Keep in Mind
- Object‑bound methods only:
spyOncannot target a standalone function declared in the global scope. Wrap such functions in an object or use a module‑mocking approach. - State leakage: Although Jasmine resets spies between
itblocks by default, manually created spies on shared objects must be restored or cleared to avoid cross‑test contamination.
Verifying the Result
After running the spec, the output should show a passing test. If you still see network activity or the original fetch being executed, the spy was not applied correctly—check that you spied on the same instance of ApiClient that the service uses and that the spy was set before invoking the unit under test.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.