Skip to main content

MockServer Testing

The MockServer is Balda's powerful testing utility that allows you to test HTTP endpoints without starting a real server. This enables fast, isolated testing of your controllers and middleware.

Overview

MockServer simulates HTTP requests by:

  • Executing the complete middleware chain
  • Running route handlers with mocked request/response objects
  • Providing realistic testing environment without network overhead
  • Supporting all HTTP methods and content types

Basic Usage

import { describe, it, expect } from "vitest";
import { Server } from "balda";

const server = new Server({ port: 3000 });

// Get MockServer instance (async operation)
const mockServer = await server.getMockServer();

describe("API Tests", () => {
it("GET /users returns all users", async () => {
const res = await mockServer.get("/users");
expect(res.statusCode()).toBe(200);
expect(Array.isArray(res.body())).toBe(true);
});
});

HTTP Methods

MockServer provides methods for all HTTP verbs:

GET Requests

// Simple GET request
const res = await mockServer.get("/users");

// GET with query parameters
const res = await mockServer.get("/users", {
query: { page: "1", limit: "10" }
});

// GET with headers
const res = await mockServer.get("/users", {
headers: { "Authorization": "Bearer token123" }
});

POST Requests

// POST with JSON body
const res = await mockServer.post("/users", {
body: { name: "John", email: "john@example.com" }
});

// POST with form data
const res = await mockServer.post("/users", {
urlencoded: { name: "John", email: "john@example.com" }
});

// POST with file upload
const formData = new FormData();
formData.append("file", new Blob(["content"]), "test.txt");
const res = await mockServer.post("/upload", { formData });

Other HTTP Methods

// PUT request
const res = await mockServer.put("/users/1", {
body: { name: "Updated Name" }
});

// PATCH request
const res = await mockServer.patch("/users/1", {
body: { name: "Patched Name" }
});

// DELETE request
const res = await mockServer.delete("/users/1");

Request Options

MockServer supports comprehensive request configuration:

interface MockServerOptions {
headers?: Record<string, string>;
query?: Record<string, string>;
cookies?: Record<string, string>;
ip?: string;
body?: any;
formData?: FormData;
urlencoded?: Record<string, string>;
}

Headers

const res = await mockServer.get("/protected", {
headers: {
"Authorization": "Bearer token123",
"Content-Type": "application/json"
}
});

Query Parameters

const res = await mockServer.get("/search", {
query: {
q: "search term",
page: "1",
limit: "20"
}
});

Cookies

const res = await mockServer.get("/profile", {
cookies: {
sessionId: "abc123",
userId: "456"
}
});

IP Address

const res = await mockServer.get("/location", {
ip: "192.168.1.1"
});

Response Assertions

MockResponse provides powerful assertion methods:

Status Code Assertions

const res = await mockServer.get("/users");
expect(res.statusCode()).toBe(200);
expect(res.assertStatus(200)); // Alternative syntax

Body Assertions

// Exact body match
expect(res.assertBodyDeepEqual({ id: 1, name: "John" }));

// Partial body match
expect(res.assertBodySubset({ name: "John" }));

// Get body content
const body = res.body();
expect(body.name).toBe("John");

Complete Example

describe("User API", () => {
it("creates a new user", async () => {
const newUser = {
name: "Jane Doe",
email: "jane@example.com",
age: 25
};

const res = await mockServer.post("/users", { body: newUser });

expect(res.assertStatus(201));
expect(res.assertBodyDeepEqual(newUser));
});

it("returns 404 for non-existent user", async () => {
const res = await mockServer.get("/users/999");

expect(res.assertStatus(404));
expect(res.assertBodyDeepEqual({ error: "User not found" }));
});
});

File Upload Testing

MockServer supports file upload testing with FormData:

describe("File Upload", () => {
it("uploads a file successfully", async () => {
const formData = new FormData();
const fileContent = new Uint8Array([1, 2, 3, 4, 5]);
formData.append("file", new Blob([fileContent]), "test.txt");

const res = await mockServer.post("/upload", { formData });

expect(res.assertStatus(200));
expect(res.body()).toEqual({
originalName: "test.txt",
filename: "file",
size: 5,
mimetype: "application/octet-stream"
});
});
});

Error Handling

MockServer properly handles and reports errors:

describe("Error Handling", () => {
it("handles server errors gracefully", async () => {
const res = await mockServer.get("/users", {
query: { shouldFail: "true" }
});

expect(res.assertStatus(500));
expect(res.body()).toHaveProperty("error");
});
});

Best Practices

1. Test Isolation

describe("User Management", () => {
// Each test should be independent
it("creates user", async () => {
// Test implementation
});

it("updates user", async () => {
// Test implementation - doesn't depend on previous test
});
});

2. Descriptive Test Names

// Good
it("POST /users returns 409 when user already exists", async () => {
// Test implementation
});

// Avoid
it("test user creation", async () => {
// Test implementation
});

3. Use TypeScript for Type Safety

See the Type-Safe Testing section below for comprehensive type safety examples.

4. Test Edge Cases

describe("User Validation", () => {
it("rejects invalid email format", async () => {
const res = await mockServer.post("/users", {
body: { name: "John", email: "invalid-email" }
});

expect(res.assertStatus(400));
});

it("requires all mandatory fields", async () => {
const res = await mockServer.post("/users", {
body: { name: "John" } // Missing email
});

expect(res.assertStatus(400));
});
});

Type-Safe Testing

MockServer supports full TypeScript type safety through explicit generic type parameters. This ensures compile-time checking of request bodies, query parameters, and response types.

Basic Type-Safe Requests

// Define your types
type User = {
id: number;
name: string;
email: string;
age: number;
};

type CreateUserInput = {
name: string;
email: string;
age: number;
};

type UserQuery = {
limit?: string;
offset?: string;
};

// Type-safe GET request
const res = await mockServer.get<User[]>("/users");
expect(res.body[0].name).toBe("John"); // Fully typed!

// Type-safe POST request with body type
const createRes = await mockServer.post<User, CreateUserInput>("/users", {
body: {
name: "Jane Doe",
email: "jane@example.com",
age: 25,
},
});
expect(createRes.body.id).toBeDefined(); // Type-safe response

// Type-safe GET with query parameters
const queryRes = await mockServer.get<User[], UserQuery>("/users", {
query: { limit: "10" },
});

HTTP Method Type Signatures

// GET - no body allowed
mockServer.get<TResponse, TQuery>(path, options?)

// POST - with body
mockServer.post<TResponse, TBody, TQuery>(path, options?)

// PUT - with body
mockServer.put<TResponse, TBody, TQuery>(path, options?)

// PATCH - with body
mockServer.patch<TResponse, TBody, TQuery>(path, options?)

// DELETE - no body allowed
mockServer.delete<TResponse, TQuery>(path, options?)

Type-Safe Error Handling

type ErrorResponse = {
error: string;
code?: string;
};

type UserResponse = User | ErrorResponse;

const res = await mockServer.get<UserResponse>("/users/999");

if (res.statusCode === 404) {
// TypeScript knows this is ErrorResponse
expect((res.body as ErrorResponse).error).toBe("User not found");
}

Organizing Test Types

For better maintainability, organize types in separate files:

// types/api.types.ts
export type User = {
id: number;
name: string;
email: string;
age: number;
};

export type CreateUserInput = Omit<User, "id">;
export type UpdateUserInput = Partial<CreateUserInput>;

export type PaginatedResponse<T> = {
data: T[];
total: number;
page: number;
};

// tests/user.test.ts
import { User, CreateUserInput, PaginatedResponse } from "../types/api.types";

const res = await mockServer.get<PaginatedResponse<User>>("/users");
expect(res.body.data).toHaveLength(10);
expect(res.body.total).toBeGreaterThan(0);

Complete Type-Safe Test Example

import { describe, it, expect } from "vitest";
import { User, CreateUserInput, UpdateUserInput } from "../types/api.types";

describe("User API - Type Safe", () => {
it("creates user with type safety", async () => {
const newUser: CreateUserInput = {
name: "John Doe",
email: "john@example.com",
age: 30,
};

const res = await mockServer.post<User, CreateUserInput>("/users", {
body: newUser,
});

expect(res.statusCode).toBe(201);
expect(res.body.name).toBe(newUser.name); // Fully typed!
expect(res.body).toHaveProperty("id"); // Response includes id
});

it("updates user with partial data", async () => {
const updates: UpdateUserInput = {
name: "Jane Doe", // Only updating name
};

const res = await mockServer.patch<User, UpdateUserInput>("/users/1", {
body: updates,
});

expect(res.statusCode).toBe(200);
expect(res.body.name).toBe(updates.name);
});

it("handles typed error responses", async () => {
type ErrorResponse = { error: string };

const res = await mockServer.get<ErrorResponse>("/users/999");

expect(res.statusCode).toBe(404);
expect(res.body.error).toBe("User not found"); // Typed error!
});
});

Benefits of Type-Safe Testing

Compile-Time Safety - Catch mistakes before running tests ✅ IDE Autocomplete - Full IntelliSense support for request/response ✅ Self-Documenting - Types serve as inline API documentation ✅ Refactor Confidence - TypeScript catches breaking changes ✅ Better Testing Experience - Less time debugging, more time coding

Advanced Features

Custom Request Configuration

const res = await mockServer.request("GET", "/api/data", {
headers: {
"X-Custom-Header": "value",
"Authorization": "Bearer token"
},
query: { filter: "active" },
cookies: { sessionId: "abc123" },
ip: "192.168.1.100"
});

Testing Middleware

MockServer executes the complete middleware chain, allowing you to test:

  • Authentication middleware
  • Rate limiting
  • CORS handling
  • Request logging
  • Custom business logic middleware

This comprehensive testing approach ensures your entire request pipeline works correctly in isolation.