TypeScript Testing
Guidance for writing effective tests in TypeScript projects using Vitest or Jest.
Rules
- Arrange-Act-Assert: Structure tests with clear setup, execution, and verification phases
- Single Assertion Focus: Each test should verify one specific behavior
- Descriptive Names: Use
describe blocks for grouping and it/test with clear descriptions
- Test Isolation: Tests must not depend on execution order or shared mutable state
- Mock External Dependencies: Always mock I/O, network calls, and system interactions
- Coverage Targets: Aim for 80%+ line coverage, 70%+ branch coverage
- No Logic in Tests: Avoid conditionals and loops in test code
- Clean Up: Use
beforeEach/afterEach for setup/teardown, never leave test artifacts
Patterns
Test Structure
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { MyService } from "./my-service.js";
describe("MyService", () => {
let service: MyService;
beforeEach(() => {
service = new MyService();
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
describe("methodName", () => {
it("should return expected result when given valid input", () => {
// Arrange
const input = "test";
// Act
const result = service.methodName(input);
// Assert
expect(result).toBe("expected");
});
it("should throw error when given invalid input", () => {
expect(() => service.methodName(null)).toThrow("Invalid input");
});
});
});
```markdown
### Mocking Dependencies
```typescript
import { vi, type Mock } from "vitest";
import type { Database } from "./database.js";
// Factory mock
vi.mock("./database.js", () => ({
Database: vi.fn().mockImplementation(() => ({
query: vi.fn(),
close: vi.fn(),
})),
}));
// Inline mock with type safety
const mockFetch = vi.fn() as Mock<typeof fetch>;
vi.stubGlobal("fetch", mockFetch);
mockFetch.mockResolvedValue(
new Response(JSON.stringify({ data: "test" }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
```markdown
### Testing Async Code
```typescript
it("should handle async operations", async () => {
const promise = service.asyncMethod();
// Advance timers if needed
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBeDefined();
});
it("should reject with error on failure", async () => {
mockDependency.mockRejectedValueOnce(new Error("Network error"));
await expect(service.asyncMethod()).rejects.toThrow("Network error");
});
```markdown
### Snapshot Testing
```typescript
it("should match snapshot for complex output", () => {
const result = service.generateConfig();
expect(result).toMatchSnapshot();
});
it("should match inline snapshot", () => {
const result = service.formatOutput({ key: "value" });
expect(result).toMatchInlineSnapshot(`"{ key: 'value' }"`);
});
```markdown
## Anti-Patterns
```typescript
// ❌ Testing implementation details
it("should call internal method", () => {
const spy = vi.spyOn(service, "_privateMethod");
service.publicMethod();
expect(spy).toHaveBeenCalled(); // Don't test internals
});
// ❌ Multiple unrelated assertions
it("should work", () => {
expect(service.method1()).toBe("a");
expect(service.method2()).toBe("b");
expect(service.method3()).toBe("c"); // Split into separate tests
});
// ❌ Test interdependence
let sharedState: string;
it("first test", () => { sharedState = "set"; });
it("depends on first", () => { expect(sharedState).toBe("set"); }); // Never do this
// ❌ Not awaiting async code
it("broken async test", () => {
service.asyncMethod().then(r => expect(r).toBe("x")); // Missing await/return
});
// ❌ Overspecified mocks
mockFn.mockReturnValueOnce("a")
.mockReturnValueOnce("b")
.mockReturnValueOnce("c"); // Fragile, breaks if call order changes
```markdown
## Examples
### Testing a Service Class
```typescript
import { describe, it, expect, vi, beforeEach } from "vitest";
import { UserService } from "./user-service.js";
import type { UserRepository } from "./user-repository.js";
describe("UserService", () => {
let service: UserService;
let mockRepo: UserRepository;
beforeEach(() => {
mockRepo = {
findById: vi.fn(),
save: vi.fn(),
delete: vi.fn(),
};
service = new UserService(mockRepo);
});
describe("getUser", () => {
it("should return user when found", async () => {
const user = { id: "1", name: "Test" };
vi.mocked(mockRepo.findById).mockResolvedValue(user);
const result = await service.getUser("1");
expect(result).toEqual(user);
expect(mockRepo.findById).toHaveBeenCalledWith("1");
});
it("should return null when user not found", async () => {
vi.mocked(mockRepo.findById).mockResolvedValue(null);
const result = await service.getUser("999");
expect(result).toBeNull();
});
});
});
```markdown
### Testing Error Handling
```typescript
describe("error handling", () => {
it("should wrap repository errors", async () => {
const dbError = new Error("Connection failed");
vi.mocked(mockRepo.findById).mockRejectedValue(dbError);
await expect(service.getUser("1")).rejects.toThrow("Failed to fetch user");
});
it("should handle validation errors gracefully", () => {
expect(() => service.validateInput("")).toThrow(ValidationError);
expect(() => service.validateInput("")).toThrow("Input cannot be empty");
});
});
References
1---2name: typescript-testing3description: Comprehensive testing guidance for TypeScript projects including unit testing patterns, mocking strategies, and test organization best practices4---56# TypeScript Testing78Guidance for writing effective tests in TypeScript projects using Vitest or Jest.910## Rules1112- **Arrange-Act-Assert**: Structure tests with clear setup, execution, and verification phases13- **Single Assertion Focus**: Each test should verify one specific behavior14- **Descriptive Names**: Use `describe` blocks for grouping and `it`/`test` with clear descriptions15- **Test Isolation**: Tests must not depend on execution order or shared mutable state16- **Mock External Dependencies**: Always mock I/O, network calls, and system interactions17- **Coverage Targets**: Aim for 80%+ line coverage, 70%+ branch coverage18- **No Logic in Tests**: Avoid conditionals and loops in test code19- **Clean Up**: Use `beforeEach`/`afterEach` for setup/teardown, never leave test artifacts2021## Patterns2223### Test Structure2425```typescript26import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";27import { MyService } from "./my-service.js";2829describe("MyService", () => {30 let service: MyService;3132 beforeEach(() => {33 service = new MyService();34 vi.useFakeTimers();35 });3637 afterEach(() => {38 vi.useRealTimers();39 vi.restoreAllMocks();40 });4142 describe("methodName", () => {43 it("should return expected result when given valid input", () => {44 // Arrange45 const input = "test";4647 // Act48 const result = service.methodName(input);4950 // Assert51 expect(result).toBe("expected");52 });5354 it("should throw error when given invalid input", () => {55 expect(() => service.methodName(null)).toThrow("Invalid input");56 });57 });58});59```markdown6061### Mocking Dependencies6263```typescript64import { vi, type Mock } from "vitest";65import type { Database } from "./database.js";6667// Factory mock68vi.mock("./database.js", () => ({69 Database: vi.fn().mockImplementation(() => ({70 query: vi.fn(),71 close: vi.fn(),72 })),73}));7475// Inline mock with type safety76const mockFetch = vi.fn() as Mock<typeof fetch>;77vi.stubGlobal("fetch", mockFetch);7879mockFetch.mockResolvedValue(80 new Response(JSON.stringify({ data: "test" }), {81 status: 200,82 headers: { "Content-Type": "application/json" },83 })84);85```markdown8687### Testing Async Code8889```typescript90it("should handle async operations", async () => {91 const promise = service.asyncMethod();92 93 // Advance timers if needed94 await vi.runAllTimersAsync();95 96 const result = await promise;97 expect(result).toBeDefined();98});99100it("should reject with error on failure", async () => {101 mockDependency.mockRejectedValueOnce(new Error("Network error"));102 103 await expect(service.asyncMethod()).rejects.toThrow("Network error");104});105```markdown106107### Snapshot Testing108109```typescript110it("should match snapshot for complex output", () => {111 const result = service.generateConfig();112 expect(result).toMatchSnapshot();113});114115it("should match inline snapshot", () => {116 const result = service.formatOutput({ key: "value" });117 expect(result).toMatchInlineSnapshot(`"{ key: 'value' }"`);118});119```markdown120121## Anti-Patterns122123```typescript124// ❌ Testing implementation details125it("should call internal method", () => {126 const spy = vi.spyOn(service, "_privateMethod");127 service.publicMethod();128 expect(spy).toHaveBeenCalled(); // Don't test internals129});130131// ❌ Multiple unrelated assertions132it("should work", () => {133 expect(service.method1()).toBe("a");134 expect(service.method2()).toBe("b");135 expect(service.method3()).toBe("c"); // Split into separate tests136});137138// ❌ Test interdependence139let sharedState: string;140it("first test", () => { sharedState = "set"; });141it("depends on first", () => { expect(sharedState).toBe("set"); }); // Never do this142143// ❌ Not awaiting async code144it("broken async test", () => {145 service.asyncMethod().then(r => expect(r).toBe("x")); // Missing await/return146});147148// ❌ Overspecified mocks149mockFn.mockReturnValueOnce("a")150 .mockReturnValueOnce("b")151 .mockReturnValueOnce("c"); // Fragile, breaks if call order changes152```markdown153154## Examples155156### Testing a Service Class157158```typescript159import { describe, it, expect, vi, beforeEach } from "vitest";160import { UserService } from "./user-service.js";161import type { UserRepository } from "./user-repository.js";162163describe("UserService", () => {164 let service: UserService;165 let mockRepo: UserRepository;166167 beforeEach(() => {168 mockRepo = {169 findById: vi.fn(),170 save: vi.fn(),171 delete: vi.fn(),172 };173 service = new UserService(mockRepo);174 });175176 describe("getUser", () => {177 it("should return user when found", async () => {178 const user = { id: "1", name: "Test" };179 vi.mocked(mockRepo.findById).mockResolvedValue(user);180181 const result = await service.getUser("1");182183 expect(result).toEqual(user);184 expect(mockRepo.findById).toHaveBeenCalledWith("1");185 });186187 it("should return null when user not found", async () => {188 vi.mocked(mockRepo.findById).mockResolvedValue(null);189190 const result = await service.getUser("999");191192 expect(result).toBeNull();193 });194 });195});196```markdown197198### Testing Error Handling199200```typescript201describe("error handling", () => {202 it("should wrap repository errors", async () => {203 const dbError = new Error("Connection failed");204 vi.mocked(mockRepo.findById).mockRejectedValue(dbError);205206 await expect(service.getUser("1")).rejects.toThrow("Failed to fetch user");207 });208209 it("should handle validation errors gracefully", () => {210 expect(() => service.validateInput("")).toThrow(ValidationError);211 expect(() => service.validateInput("")).toThrow("Input cannot be empty");212 });213});214```215216## References217218- [Vitest Documentation](https://vitest.dev/)219- [Jest Documentation](https://jestjs.io/)220- [Testing Library](https://testing-library.com/)221- [MSW (Mock Service Worker)](https://mswjs.io/)