Full Stack Development

Spying and Mocking



by Tomas Trescak t.trescak@westernsydney.edu.au

Spies

/

Introduction

  • Why spy or mock?
  • 👺 Why "test" mocks? It's fake!

  • Spies observe functions
  • Spies gather information on function calls
  • 👺 But spies break functions!

Spies

/

Example

    // tests/posts.test.ts
import { describe, it, expect, vi } from "vitest";
import { getPostsByTag } from "../utils/posts";
import { prisma } from "../utils/client"

describe("getPostsByTag", () => {
  it("calls getPostsByTag with correct tag", () => {
    // Arrange
    const spy = vi.spyOn(prisma.posts, "findMany");
    
    // Act
    getPostsByTag("tech"); 
    
    // Assert
    expect(spy).toHaveBeenCalledWith({
      where: { tag: "tech" }
    }); 
  });
});
  

Spies

/

Matchers

    it("logs view", () => {
  const log = vi.spyOn(console, "log");
  console.log("View"); 
  expect(log).toHaveBeenCalled(); 
});
  
toHaveBeenCalled
how many the spy has been called

Spies

/

Matchers

    it("retries twice", () => {
  const spy = vi.spyOn({ getPostsByTag }, "getPostsByTag");
  getPostsByTag("tech");
  getPostsByTag("tech"); 
  expect(spy).toHaveBeenCalledTimes(2); 
});
  
toHaveBeenCalledTimes
Ensures exact call count, great for retry logic in failed queries

Spies

/

Matchers

    it("sorts by date", () => {
  const sort = vi.fn();
  const spy = vi.spyOn({ sort }, "sort");
  sort("tag", "ascending"); 
  sort("date", "descending"); 
  expect(spy).toHaveBeenCalledWith("date", "ascending"); 
  expect(spy).not.toHaveBeenCalledWith("date", "descending"); 
  expect(spy).toHaveBeenCalledWith("tag", "descending"); 
});
  
toHaveBeenCalledWith
Verifies arguments, key for dynamic inputs like tags

Spies

/

Matchers

    it("sorts by date last", () => {
  const sort = vi.fn();
  const spy = vi.spyOn({ sort }, "sort");
  sort("tag"); 
  sort("date"); 
  expect(spy).toHaveBeenLastCalledWith("date"); 
});
  
toHaveBeenLastCalledWith
Checks the last call’s arguments, useful for sequences.

Spies

/

Example

    // tests/admin.test.ts
import { describe, it, expect, vi } from "vitest";
import { bulkUpdateTags } from "../src/admin";
import * as posts from "../src/utils/posts";

describe("bulkUpdateTags", () => {
  it("updates multiple posts", () => {
    const spy = vi.spyOn(posts, "updatePost"); 
    bulkUpdateTags([1, 2], "news"); 
    expect(spy).toHaveBeenCalledTimes(2); 
    expect(spy).toHaveBeenCalledWith(1, { tag: "news" }); 
    expect(spy).toHaveBeenLastCalledWith(2, { tag: "news" });
  });
});
  

Spies

/

Takeaways

  • Spies track behaviour
  • Best Practices
    • vi.restoreAllMocks()
    • spy.restore()
  • Pitfalls

Mocks

/

Introduction

  • Mocks replace code
  • Mocks = speed and control
  • 👺 Mocking fakes the whole app

Mocks

/

Example

    // tests/posts.test.ts
import { describe, it, expect, vi } from "vitest";
import { getPostsByTag } from "../utils/posts";
import { prisma } from "../utils/client";

describe("getPostsByTag", () => {
  it("returns mocked posts", async () => {
    vi.mock(prisma.posts, "findMany").mockResolvedValue([
      { id: 1, title: "Mocked Post", tag: "tech" },
    ]); 
    const posts = await getPostsByTag("tech"); 
    expect(posts).toEqual([{ 
      id: 1, title: "Mocked Post", 
      tag: "tech" 
    }]); 
  });
});
  

Mocks

/

Methods

    // Mock local import
const validate = new DataValidation();
vi.spyOn(validate, "validatePost").mockReturnValue(true);

// Mock local variables
vi.mocked(formatTag)
    .mockImplementation((tag) => tag.toUpperCase()); 

// Mock node module
vi.mock("db-client", () => ({
  query: vi.fn().mockReturnValue([{ id: 1 }]),
})); 

// Mock whole module
vi.mock("../src/utils/posts", () => ({
  getPostsByTag: vi.fn().mockReturnValue([{ id: 1 }]),
}));
  

Mocks

/

Options

    // Calls
vi.mocked(savePost).mockReturnValue({ id: 1 });
savePost({ title: "Test" });
expect(savePost.mock.calls[0][0]).toEqual({ title: "Test" }); 
expect(savePost).toHaveBeenCalledWith({ title: "Test" })

// Functions
vi.mocked(formatTag).mockImplementation(
    (tag) => tag.toUpperCase()
); 

// Promises
vi.mocked(savePost).mockRejectedValue(new Error("DB error")); 
  

Mocks

/

Example

    import { describe, it, expect, vi } from "vitest";
import { createPost } from "../src/admin";
import * as posts from "../src/utils/posts";
import * as validate from "../src/utils/validate";

describe("createPost", () => {
  it("saves valid post", async () => {
    // Arrange
    vi.spyOn(validate, "validatePost").mockReturnValue(true); 
    vi.spyOn(posts, "savePost").mockResolvedValue({ 
      id: 42, 
      title: "Test" 
    }); 
    
    // Act
    const post = { title: "Test", tag: "tech" };
    const result = await createPost(post); 
    
    // Assert
    expect(validate.validatePost).toHaveBeenCalledWith(post); 
    expect(posts.savePost).toHaveBeenCalledWith(post); 
    expect(result).toEqual({ id: 42, title: "Test" });
  });
});
  

Mocks

/

To mock or not to mock ...

  • Mock for isolation
  • Don’t mock internals
  • Overmocking