Jasmine spy state persistence across it blocks with jasmine.createSpy
0 reputation · 24 Sept 2023, 15:09 UTC
0 reputation · 24 Sept 2023, 15:09 UTC
The goal is to clarify how Jasmine manages spy lifecycle and test isolation when the same spy instance is reused across multiple it blocks within a describe suite.
Spies created with jasmine.createSpy and jasmine.createSpyObj track calls, return values and call order, and the framework does not enforce automatic suite-level isolation for shared references. State persistence can lead to call history accumulation unless cleanup is applied explicitly, and the exact boundaries for automatic reset versus manual cleanup are not clearly documented.
This matters for designing reliable specs that avoid leakage between tests, especially when spies are created at describe scope or shared across files. Behavior is version sensitive and the expected defaults for isolation are uncertain.
Does Jasmine automatically reset calls history for a spy between it blocks? Which creation scope for jasmine.createSpy or jasmine.createSpyObj guarantees isolation without manual reset? When is explicit cleanup required to prevent state leakage across specs?
31775 reputation · 24 Sept 2023, 16:53 UTC
it blocks?Spies created with jasmine.createSpy or jasmine.createSpyObj are plain JavaScript functions that keep an internal array of calls. The Jasmine test runner does not touch that array when the test suite finishes a test. Consequently, if the spy is defined in a describe scope and reused, its call history persists across all it blocks that run in that suite.
it block are freshly instantiated for every test.beforeEach block are recreated before each test, so their history starts at zero.describe block and never reassigned keep the same instance for the entire suite.describe('Service', () => {\n let fetchSpy;\n\n beforeEach(() => {\n fetchSpy = jasmine.createSpy('fetch');\n });\n\n it('calls fetch once', () => {\n fetchSpy();\n expect(fetchSpy).toHaveBeenCalledTimes(1);\n });\n\n it('starts fresh', () => {\n expect(fetchSpy).not.toHaveBeenCalled();\n });\n});\nIf a spy is created once in a describe block and that reference is shared across tests, you must either re‑assign it in a beforeEach or explicitly reset it with spy.calls.reset() at the end of a test.
describe scope.it.not.toHaveBeenCalled() in the second it.Ask yourself: Where is the spy defined? If it lives outside beforeEach or it, you’re likely to see leakage.
Use comments to ask for clarification. Post a solution as an answer.
31,775 reputation · 24 Sept 2023, 19:22 UTC
When a spy created with jasmine.createSpy (or jasmine.createSpyObj) is reused across it blocks, two kinds of state accumulate:
spy.calls (count, args, order).spy.and.returnValue, spy.and.callFake, spy.and.callThrough, etc.Calling spy.calls.reset() (or clear()) only empties the call history; it does not restore the original function or remove custom return values. To fully isolate a spy you must also reset its stub configuration with spy.and.reset() (or re‑assign the spy in a beforeEach).
Example:
describe('Shared spy', () => {
let mySpy;
beforeEach(() => {
mySpy = jasmine.createSpy('mySpy');
mySpy.and.returnValue(42);
});
afterEach(() => {
// Reset both call log and stub behavior
mySpy.calls.reset();
mySpy.and.reset();
});
it('first test', () => {
expect(mySpy()).toBe(42);
expect(mySpy).toHaveBeenCalledTimes(1);
});
it('second test starts clean', () => {
expect(mySpy).not.toHaveBeenCalled();
expect(mySpy()).toBeUndefined(); // stub cleared
});
});
This two‑step reset guarantees that each spec sees a fresh spy with no accumulated calls or lingering stub behavior, regardless of whether the spy is defined in describe scope or shared across files.