Troubleshooting Jasmine Spies: A Diagnostic Guide for Common Pitfalls
When unit tests fail because Jasmine spies aren’t behaving as expected, a systematic diagnostic approach can save hours of debugging. This guide walks through the most common spy issues, shows how to verify each step, and offers clear fixes and escalation paths.
03 Aug 2026, 19:19 UTC

Why Spies Fail in Jasmine
Spies replace the original implementation of a method with a mock that records calls. When a test reports “spy not called” or throws an error about spying on an undefined property, the underlying problem is usually a mismatch between the spy’s target and the actual code path. The following diagnostic guide lets you pinpoint and fix these issues quickly.
Common Spy‑Related Symptoms
- Spy not invoked – the call count stays at zero.
- spyOn on undefined – Jasmine throws an exception during test setup.
- Spies persist across specs – subsequent tests see a spy that was never restored.
- Async code ignores spy – the test finishes before the spy records the call.
Diagnostic Table
| Symptom | Possible Cause | Immediate Check |
|---|---|---|
| Spy not invoked | Wrong object or method rebound after spyOn | Verify the spy is attached to the same instance that the code uses. |
| spyOn on undefined | Property missing or not a function at spy creation time | Check the object's prototype chain and initialization order. |
| Spies persist across specs | Spy not restored, shared objects used | Run a fresh spec with a new spy instance and compare results. |
| Async code ignores spy | Test finishes before async call records the spy | Use done callbacks or async/await to wait for completion. |
Step‑by‑Step Checks and Fixes
1. Confirm Spy Attachment
Make sure the spy is attached to the exact object instance that the code under test uses.
class Service {
fetch() { /* real implementation */ }
}
describe('Service', () => {
let service;
beforeEach(() => {
service = new Service();
spyOn(service, 'fetch').and.returnValue(Promise.resolve('ok'));
});
it('calls fetch', () => {
service.fetch();
expect(service.fetch).toHaveBeenCalled();
});
});
If the code uses a shared prototype method, spyOn the prototype instead:
spyOn(Service.prototype, 'fetch').and.callThrough();
2. Verify Property Exists at Spy Time
Spying on an undefined property throws an error immediately. Add a guard or create the property before spying.
beforeEach(() => {
if (!service.fetch) {
service.fetch = () => {};
}
spyOn(service, 'fetch').and.callThrough();
});
3. Ensure Spy Restoration Between Specs
Use afterEach to restore spies or create fresh spy objects in beforeEach. This prevents state leakage.
let spy;
beforeEach(() => {
spy = jasmine.createSpyObj('api', ['get']);
// inject spy into the module under test
});
afterEach(() => {
// Jasmine automatically restores spies created with jasmine.createSpyObj
});
If you use spyOn directly, restore manually:
afterEach(() => {
if (service.fetch.and && service.fetch.and.originalFn) {
service.fetch.and.callThrough();
}
});
4. Handle Asynchronous Calls
When the spied method is called asynchronously, the test may finish early. Use done or async/await to wait.
it('calls fetch asynchronously', (done) => {
service.fetch();
setTimeout(() => {
expect(service.fetch).toHaveBeenCalled();
done();
}, 0);
});
Alternatively, use async/await if the method returns a promise:
it('calls fetch asynchronously', async () => {
await service.fetch();
expect(service.fetch).toHaveBeenCalled();
});
Verification Techniques
- Print the spy call count:
console.log('fetch calls:', service.fetch.calls.count()); - Check if any call occurred:
expect(service.fetch.calls.any()).toBeTrue(); - Run the suite with
--no-colorand grep for the spy name to confirm it appears in the raw output.
Escalation Criteria
If after applying the above steps the test still fails, consider:
- Test Environment – Ensure no global mocks or test runners (e.g., Jest) are overriding Jasmine’s behavior.
- Prototype Pollution – Verify that no other spec is adding properties to the same prototype that could interfere.
- External Dependencies – If the spy is on a module loaded via
require, use dependency injection or a module mocking library (e.g.,proxyquire) to isolate the spy. - Jasmine Version – Some older Jasmine releases had bugs with
spyOnon getters/setters. Upgrade to the latest stable release if possible.
Limitations and Caveats
- Spies replace the original implementation; if you rely on side effects, use
and.callThrough()to preserve behavior. - Spying on shared prototypes can affect unrelated specs. Prefer instance spies or
jasmine.createSpyObj. - Async timing issues may still surface if the test framework’s event loop is misbehaving. In such cases, use
jasmine.clock().install()to control timers.
By following this diagnostic workflow, you can quickly isolate whether a spy is attached incorrectly, not restored, or out of sync with asynchronous code, and apply the appropriate fix before escalating to the team or the framework maintainers.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.