---
title: "How and When to Use Mocks vs Spies in Unit Testing (Vitest, Jest, and Jasmine)"
date: "2022-12-30"
slug: "how-and-when-to-use-mocks-or-spies-on-unit-testing"
author: "Dany Paredes"
canonical: "https://danywalls.com/how-and-when-to-use-mocks-or-spies-on-unit-testing"
description: "Learn the practical differences between Mocks and Spies in unit testing. Explore practical examples using modern Vitest (vi.fn, vi.spyOn), Jest, and Jasmine in Angular."
---


When writing unit tests for Angular services or TypeScript domain classes, you rarely test functions in total isolation. Your classes have dependencies: HTTP clients, authentication providers, tax calculators, or third-party SDKs.

To prevent tests from hitting real servers or breaking when secondary logic changes, we use **Test Doubles**. Two of the most common are **Mocks** and **Spies**.

> ⚡ **Using Modern Angular with Vitest or Jest?** Jump directly to [Modern Test Runners: Mocking & Spying in Vitest and Jest](#modern-test-runners-mocking-and-spying-in-vitest-and-jest) to see how to use `vi.fn()` and `vi.spyOn()`.

In this guide, we will clarify the difference between Mocks and Spies, review practical scenarios, and explore examples across **Vitest**, **Jest**, and **Jasmine**.

Let's begin by defining our test scenario.

---

## 1. The Scenario: Invoice Processing Service 🧾

Imagine an `InvoiceService` that calculates an invoice total based on tax rules and submits the completed invoice to an external government API:

```typescript
// invoice.service.ts
import { Injectable, inject } from '@angular/core';

export class TaxCalculationService {
  getTaxRate(): number {
    // In real life, this might make an HTTP call to a tax service
    return 0.18;
  }
}

export class GovernmentExportService {
  sendToGovernment(invoiceId: number): boolean {
    console.log(`Sending invoice ${invoiceId} to government gateway...`);
    return true;
  }
}

export class InvoiceService {
  id: number = Math.floor(Math.random() * 1000000);
  isProcessed: boolean = false;

  constructor(
    private taxService: TaxCalculationService,
    private exportService: GovernmentExportService
  ) {}

  calculateTotal(subtotal: number): number {
    const rate = this.taxService.getTaxRate();
    return subtotal + (subtotal * rate);
  }

  processInvoice(): boolean {
    const success = this.exportService.sendToGovernment(this.id);
    if (success) {
      this.isProcessed = true;
    }
    return this.isProcessed;
  }
}
```

### The Problem with Testing Using Real Instances

If we test `InvoiceService` by instantiating the real `TaxCalculationService` and `GovernmentExportService`:
1. If the tax rate calculation logic changes, our invoice tests break (brittle tests).
2. The `GovernmentExportService` might attempt real network operations or side effects during test runs.

Let's look at how Mocks solve the first problem.

---

## 2. Using Mocks: Controlling Return Values 🎭

A **Mock** replaces a dependency with a controlled fake object. You dictate what values it returns so you can verify how your unit handles specific outputs.

### When to use a Mock:
- When you want to isolate your class from external calculations or HTTP responses.
- When you want to test edge cases (e.g. tax rate is `0`, API returns `404`, or service throws an error).

### Example with Jasmine (`jasmine.createSpyObj`)

```typescript
import { InvoiceService, TaxCalculationService, GovernmentExportService } from './invoice.service';

describe('InvoiceService with Mocks (Jasmine)', () => {
  let invoiceService: InvoiceService;
  let mockTaxService: jasmine.SpyObj<TaxCalculationService>;
  let mockExportService: jasmine.SpyObj<GovernmentExportService>;

  beforeEach(() => {
    // 1. Create mock objects with designated methods
    mockTaxService = jasmine.createSpyObj<TaxCalculationService>(['getTaxRate']);
    mockExportService = jasmine.createSpyObj<GovernmentExportService>(['sendToGovernment']);

    // 2. Control return values
    mockTaxService.getTaxRate.and.returnValue(0.20); // 20% tax

    // 3. Inject mocks into the service
    invoiceService = new InvoiceService(mockTaxService, mockExportService);
  });

  it('should calculate total with mocked tax rate', () => {
    const total = invoiceService.calculateTotal(100);
    expect(total).toBe(120); // 100 + (100 * 0.20)
  });
});
```

Now let's see how Spies differ when you only need to observe function invocations.

---

## 3. Using Spies: Verifying Execution and Arguments 🕵️‍♂️

A **Spy** monitors function executions. It observes:
- Was the method called?
- How many times was it called?
- What exact arguments were passed to it?

### When to use a Spy:
- When you don't necessarily need to replace the returned data, but want to verify that a secondary action (e.g. logging, analytics dispatch, saving to storage) occurred with the correct parameters.

### Example with Jasmine (`spyOn`)

```typescript
it('should verify sendToGovernment was called with invoice ID', () => {
  mockExportService.sendToGovernment.and.returnValue(true);

  const result = invoiceService.processInvoice();

  // Verify execution and exact arguments
  expect(mockExportService.sendToGovernment).toHaveBeenCalledTimes(1);
  expect(mockExportService.sendToGovernment).toHaveBeenCalledWith(invoiceService.id);
  expect(result).toBe(true);
});
```

Now let's examine modern testing in Angular using Vitest.

---

## 4. Modern Test Runners: Mocking and Spying in Vitest and Jest ⚡

Modern Angular (v18 and v19+) provides first-class support for **Vitest** (via `@angular/build` and Vite tooling) and **Jest**, replacing the deprecated Karma/Protractor setup.

In Vitest and Jest, the API is concise, unified, and standard across TypeScript.

### Mocking and Spying with Vitest (`vi.fn` and `vi.spyOn`)

```typescript
// invoice.service.spec.ts (Vitest)
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { InvoiceService, TaxCalculationService, GovernmentExportService } from './invoice.service';

describe('InvoiceService (Vitest)', () => {
  let invoiceService: InvoiceService;
  let mockTaxService: Partial<TaxCalculationService>;
  let mockExportService: Partial<GovernmentExportService>;

  beforeEach(() => {
    // 1. Create mock with vi.fn()
    mockTaxService = {
      getTaxRate: vi.fn().mockReturnValue(0.10), // 10% tax
    };

    mockExportService = {
      sendToGovernment: vi.fn().mockReturnValue(true),
    };

    invoiceService = new InvoiceService(
      mockTaxService as TaxCalculationService,
      mockExportService as GovernmentExportService
    );
  });

  it('should calculate total with Vitest mock', () => {
    const total = invoiceService.calculateTotal(200);
    expect(total).toBe(220);
    expect(mockTaxService.getTaxRate).toHaveBeenCalledOnce();
  });

  it('should spy on government export execution', () => {
    invoiceService.processInvoice();

    expect(mockExportService.sendToGovernment).toHaveBeenCalledWith(invoiceService.id);
    expect(invoiceService.isProcessed).toBe(true);
  });
});
```

### Quick Syntax Conversion Guide

| Action | Jasmine (Karma) | Jest | Vitest (Modern Angular) |
| :--- | :--- | :--- | :--- |
| **Create Mock Function** | `jasmine.createSpy()` | `jest.fn()` | `vi.fn()` |
| **Mock Return Value** | `.and.returnValue(val)` | `.mockReturnValue(val)` | `.mockReturnValue(val)` |
| **Spy on Method** | `spyOn(obj, 'method')` | `jest.spyOn(obj, 'method')` | `vi.spyOn(obj, 'method')` |
| **Verify Called Times** | `expect(s).toHaveBeenCalledTimes(1)` | `expect(s).toHaveBeenCalledTimes(1)` | `expect(s).toHaveBeenCalledOnce()` |

---

## Summary Comparison: Mock vs Spy 📊

| Characteristic | Mock 🎭 | Spy 🕵️‍♂️ |
| :--- | :--- | :--- |
| **Primary Goal** | Replace dependency and control returned output | Observe and record method invocations |
| **Underlying Implementation** | Canned / simulated object | Wraps existing method or records calls |
| **Verification Focus** | State verification (Did the class compute output correctly?) | Behavior verification (Did the class call the collaborator?) |
| **Best Scenario** | HTTP APIs, complex calculations, database calls | Logging, event dispatching, notifications |

---

## Recap 🛠️

To write clean, non-brittle unit tests:
1. Use **Mocks (`vi.fn()` / `jest.fn()`)** to simulate external service responses and test edge-case outputs.
2. Use **Spies (`vi.spyOn()`)** to verify that methods are triggered with the right arguments.
3. In modern Angular projects, embrace **Vitest** for fast, native ESM test execution.

For more Angular testing guides, check out my articles on [Testing Components in Angular with ng-mocks](/testing-components-in-angular-noerrorsschema-stub-components-and-ngmocks) and [How to Test Components Using TestBed](/how-to-test-components-in-angular-using-testbed)!

