Jasmine createSpy Diagnostics: When Spies Don't Record Calls or Return Values
A diagnostic guide for Jasmine createSpy when spies don't record calls or return values. Covers createSpy vs spyOn, injection pitfalls, behavior configuration with and.returnValue, and verification with toHaveBeenCalledTimes.
19 Mar 2026, 22:15 UTC

The problem you're seeing
A test using jasmine.createSpy either records no calls, returns undefined instead of the stubbed value, or the real implementation runs when you expected a fake. The useful takeaway: createSpy makes a standalone spy object. It does not patch anything by itself. If the subject under test never receives the spy, call metadata will be empty and return values will be wrong.
How createSpy differs from spyOn
jasmine.createSpy(name) returns a fresh spy function that records call count, arguments and return value without executing original code. Behavior is configured with the and chain: and.returnValue(), and.callThrough(), and.throwError().
jasmine.spyOn(obj, 'method') replaces an existing method on an object in place. It will throw if the method does not exist on the object in the current scope. Use spyOn for patching, createSpy for manual injection.
Recognizable conditions
- Expectation
toHaveBeenCalledTimesfails with 0 calls even though code ran. - Subject returns
undefineddespiteand.returnValue()being set. - Real side effects occur during a test that should be isolated.
spyOnthrows "method does not exist" at setup.
Cause / diagnostic table
| Symptom | Likely cause | Quick check |
|---|---|---|
| Zero calls recorded | Spy not injected into subject | Is the spy passed as a dependency or assigned to the property the subject uses? |
| Return value is undefined | Behavior not configured, or configured on wrong spy instance | Check and.returnValue is chained on the same spy reference used by subject |
| Real code executes | and.callThrough() used, or spy never replaced original | Inspect spy configuration; verify spyOn target exists |
| spyOn throws | Target method missing or accessed before definition | Confirm property exists on object at setup time |
Ordered checks
1. Confirm spy creation and injection
Run this check in your spec file where the test is defined. No special permissions required.
// create a standalone spy
const fetchUser = jasmine.createSpy('fetchUser').and.returnValue(Promise.resolve({id: 1}));
// inject it manually
const service = new UserService(fetchUser);
// exercise subject
await service.getCurrent();
// verify metadata
expect(fetchUser).toHaveBeenCalledTimes(1);
expect(fetchUser).toHaveBeenCalledWith('current');
Expected check: call count and arguments match. Risk: If UserService imports its own fetchUser internally, manual injection has no effect. createSpy does not auto-mock imported modules.
2. Verify behavior configuration
Ensure the and chain is set on the spy instance that the subject actually calls.
const spy = jasmine.createSpy('save').and.returnValue(true);
// not: const spy = jasmine.createSpy('save'); spy.and.returnValue(true) on a different reference
Expected check: subject output reflects the stubbed return. Verify by asserting subject state, not only spy metadata.
3. Distinguish spyOn vs createSpy usage
If you need to replace a method on an existing object:
const obj = { save() { /* real */ } };
spyOn(obj, 'save').and.returnValue(false);
Expected check: obj.save is now a spy. Risk: spyOn throws if save is undefined at setup. Overusing and.callThrough() can cause unintended side effects by executing real implementation logic during tests.
4. Check call metadata assertions
Use the standard matchers to confirm interaction:
expect(spy).toHaveBeenTimes(n);
expect(spy).toHaveBeenCalledWith(arg1, arg2);
expect(spy).toHaveBeenCalled();
Also assert the subject's output or state after spy invocation to confirm correct return value propagation.
Fixes tied to findings
- Spy not injected. Pass the spy via constructor, setter, or property assignment before exercising subject. For modules, refactor to accept dependency or use a test harness.
- Wrong return value. Move
and.returnValue(),and.resolveValue()orand.throwError()to the exact spy reference used. Re-create spy if it was cloned. - Real implementation runs. Remove
and.callThrough()unless intentional. If you need partial behavior, create a spy for the dependency and inject it instead of calling through. - spyOn throws. Ensure the target object and method exist at setup time. Define the object before
beforeEach, or usecreateSpywith manual injection.
Limitations and practical verification
createSpy is stable across Jasmine 3.x and 4.x for the APIs described. It records calls but does not mock the module system. Spies created with createSpy must be manually attached to the subject under test; they do not auto-mock imported modules.
Practical way to check result: after the test action, assert both spy metadata and subject output. For example, verify service.result equals the stubbed value and fetchUser was called with expected arguments.
Escalation criteria
Escalate for review if:
- Call metadata is correct but subject state is still wrong, indicating logic error beyond mocking.
- You need to mock ES module imports without refactoring. Jasmine core does not provide automatic module mocking; consider test design changes.
- Tests become flaky due to shared spy instances across specs. Ensure spies are created in
beforeEachand not reused.
Rollback is only needed if you changed production code to accept injected dependencies solely for testing. Keep changes minimal and reversible.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.