Full Stack Development
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