Isolating Unit Dependencies with Jasmine Spies
Learn how to use Jasmine Spies to isolate units of code, replace real dependencies with mocks, and verify function calls without triggering side effects.
01 Nov 2025, 12:11 UTC

The Problem: Untestable Side Effects
When writing unit tests, a primary challenge is isolating the logic of a single function from its dependencies. If a function calls a database, triggers a network request, or modifies a global state, the test becomes an integration test. This introduces flakiness, slow execution times, and the risk of mutating production data during a test run.
The solution is to replace these dependencies with Spies. A spy is a mock function that intercepts calls, records arguments, and returns predefined values without executing the original underlying logic. This creates a trust boundary between the unit under test and the external environment.
Design Requirements for Isolation
To effectively isolate a unit of code, the testing mechanism must meet three criteria:
- Interception: The ability to stop a call to a real method and redirect it to a mock.
- Inspection: A record of how many times the method was called and with which specific arguments.
- Control: The ability to force the dependency to return a specific value, throw an error, or return a rejected promise to test edge cases.
Implementing Spies: Three Common Patterns
Depending on whether the dependency is an existing object method or a standalone function, Jasmine provides different implementation paths. These examples assume Jasmine 3.0+ is configured in a Node.js or Browser environment.
1. Attaching to Existing Objects (spyOn)
Use spyOn when you need to track a method on an existing object. This is the most common approach for service-layer dependencies.
// Run in test environment with Jasmine installed
// Required permissions: Standard execution rights for the test runner
const userService = {
saveUser: (user) => { /* Real DB logic here */ }
};
// Attach spy to the saveUser method
spyOn(userService, 'saveUser').and.returnValue(true);
// Execute the unit under test
userService.saveUser({ name: 'Alice' });
// Verification
expect(userService.saveUser).toHaveBeenCalledWith({ name: 'Alice' });
2. Creating Standalone Mocks (createSpy)
Use jasmine.createSpy when the unit under test requires a callback function that doesn't exist on a pre-defined object.
const callback = jasmine.createSpy('successCallback');
const processor = (fn) => fn('Done');
processor(callback);
expect(callback).toHaveBeenCalledWith('Done');
3. Recording while Executing (callThrough)
In rare cases, you need to verify that a call happened but still want the original logic to run. This is achieved via .and.callThrough(). Risk: This breaks the unit isolation and can introduce side effects into your test suite.
Operational Checks and Failure Modes
To ensure the spy is functioning as intended, verify the following conditions:
| Scenario | Configuration | Expected Result |
|---|---|---|
| Success Path | .and.returnValue(data) |
Function returns data immediately; real logic is skipped. |
| Error Handling | .and.throwError('Fail') |
Unit under test catches the error or fails as expected. |
| Async Failure | .and.returnValue(Promise.reject('Err')) |
The .catch() block of the unit under test is triggered. |
Common Failure Modes
- Brittle Tests: If you spy on every internal private method, your tests will break every time you refactor the code, even if the output remains correct. Focus spies on the boundaries of the unit.
- State Leakage: Spies attached to global objects (like
windoworprocess) can persist across tests. Ensure you are usingbeforeEachandafterEachblocks to reset the environment.
Design Limitations and Evolution
The spy-based design is suitable for most synchronous and asynchronous JS applications. However, the design should change if:
- Complex State Machines: If the dependency has a complex internal state that affects subsequent calls, a Fake (a lightweight implementation of the class) is more appropriate than a Spy.
- External Process Dependencies: If the dependency is a separate OS process or a binary, Jasmine spies cannot intercept these; you must wrap the process call in a JS wrapper class and spy on that wrapper.
Verification and Rollback
To verify the spy is active, check the .calls property of the spy object. If userService.saveUser.calls.any() is false after execution, the spy was not correctly attached or the code path was not hit.
Rollback: Jasmine automatically cleans up spyOn mocks after each it block if they were created within that block. If you manually modified a prototype or a global object, you must manually restore the original function in the afterEach block to prevent cross-test contamination.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.