Skip to content

Commit d0f6d1c

Browse files
Phase 3: Consolidate test mock patterns and shared test data
Co-authored-by: chrisreddington <[email protected]>
1 parent ee6012f commit d0f6d1c

File tree

6 files changed

+335
-43
lines changed

6 files changed

+335
-43
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* Shared API Test Utilities
3+
*
4+
* Provides reusable mock factories and test utilities for API route testing
5+
* to eliminate duplication across test files.
6+
*/
7+
8+
import { vi } from 'vitest'
9+
import type { GameSession, Player, BaseGameState } from '../types/game'
10+
import type { TicTacToeGameState, RPSGameState, TicTacToeMove, RPSMove } from '../types/games'
11+
12+
/**
13+
* Generic game mock factory
14+
* Creates a mock game instance with all required methods
15+
*/
16+
export function createGameMock() {
17+
return {
18+
getInitialState: vi.fn(),
19+
validateMove: vi.fn(),
20+
applyMove: vi.fn(),
21+
checkGameEnd: vi.fn(),
22+
getValidMoves: vi.fn()
23+
}
24+
}
25+
26+
/**
27+
* Create mock players for testing
28+
*/
29+
export function createMockPlayers(): Player[] {
30+
return [
31+
{ id: 'player1', name: 'Player', isAI: false },
32+
{ id: 'ai', name: 'AI', isAI: true }
33+
]
34+
}
35+
36+
/**
37+
* Create a mock TicTacToe game state
38+
*/
39+
export function createMockTicTacToeGameState(overrides: Partial<TicTacToeGameState> = {}): TicTacToeGameState {
40+
return {
41+
id: 'test-game-1',
42+
players: createMockPlayers(),
43+
currentPlayerId: 'player1',
44+
status: 'playing',
45+
createdAt: new Date('2024-01-01T10:00:00Z'),
46+
updatedAt: new Date('2024-01-01T10:00:00Z'),
47+
board: [
48+
[null, null, null],
49+
[null, null, null],
50+
[null, null, null]
51+
],
52+
playerSymbols: {
53+
player1: 'X',
54+
ai: 'O'
55+
},
56+
...overrides
57+
}
58+
}
59+
60+
/**
61+
* Create a mock Rock Paper Scissors game state
62+
*/
63+
export function createMockRPSGameState(overrides: Partial<RPSGameState> = {}): RPSGameState {
64+
return {
65+
id: 'test-rps-1',
66+
players: createMockPlayers(),
67+
currentPlayerId: 'player1',
68+
status: 'playing',
69+
createdAt: new Date('2024-01-01T10:00:00Z'),
70+
updatedAt: new Date('2024-01-01T10:00:00Z'),
71+
rounds: [],
72+
currentRound: 0,
73+
maxRounds: 3,
74+
scores: {
75+
player1: 0,
76+
ai: 0
77+
},
78+
...overrides
79+
}
80+
}
81+
82+
/**
83+
* Create a mock game session
84+
*/
85+
export function createMockGameSession<T extends BaseGameState>(gameState: T, gameType: 'tic-tac-toe' | 'rock-paper-scissors'): GameSession<T> {
86+
return {
87+
gameState,
88+
gameType,
89+
history: [],
90+
aiDifficulty: 'medium'
91+
}
92+
}
93+
94+
/**
95+
* Vitest mock configuration for shared package games
96+
* Use this to create consistent mocks across API route tests
97+
*/
98+
export function createSharedGameMocks(gameClass: string) {
99+
const mockGame = createGameMock()
100+
101+
return {
102+
mockImplementation: {
103+
...vi.importActual('@turn-based-mcp/shared'),
104+
[gameClass]: vi.fn().mockImplementation(() => mockGame)
105+
},
106+
mockGame
107+
}
108+
}
109+
110+
/**
111+
* Create storage function mocks for a specific game type
112+
*/
113+
export function createStorageMocks(gameType: 'tic-tac-toe' | 'rock-paper-scissors') {
114+
if (gameType === 'tic-tac-toe') {
115+
return {
116+
getTicTacToeGame: vi.fn(),
117+
setTicTacToeGame: vi.fn(),
118+
getAllTicTacToeGames: vi.fn()
119+
}
120+
} else {
121+
return {
122+
getRPSGame: vi.fn(),
123+
setRPSGame: vi.fn(),
124+
getAllRPSGames: vi.fn()
125+
}
126+
}
127+
}
128+
129+
/**
130+
* Standard test moves for different games
131+
*/
132+
export const TEST_MOVES = {
133+
ticTacToe: { row: 0, col: 0 } as TicTacToeMove,
134+
rockPaperScissors: { choice: 'rock' } as RPSMove
135+
}

shared/src/testing/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,14 @@ export {
1212
getTestDatabase,
1313
isTestDatabaseReady
1414
} from './test-database'
15+
16+
export {
17+
createGameMock,
18+
createMockPlayers,
19+
createMockTicTacToeGameState,
20+
createMockRPSGameState,
21+
createMockGameSession,
22+
createSharedGameMocks,
23+
createStorageMocks,
24+
TEST_MOVES
25+
} from './api-test-utils'

web/src/app/api/games/rock-paper-scissors/route.test.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { vi } from 'vitest'
22
import { NextRequest } from 'next/server';
33
import type { GameSession, RPSGameState } from '@turn-based-mcp/shared';
4+
import { createRPSTestState } from '../../../../test-utils/common-test-data';
45

56
// Use vi.hoisted() to ensure the mock object is available during hoisting
67
const mockGame = vi.hoisted(() => ({
@@ -33,34 +34,15 @@ import { GET, POST } from './route';
3334
const mockGameStorage = vi.mocked(gameStorage);
3435

3536
describe('/api/games/rock-paper-scissors', () => {
36-
// Create the mock game state at module level
37-
const createMockGameState = (): RPSGameState => ({
38-
id: 'test-rps-1',
39-
players: [
40-
{ id: 'player1' as const, name: 'Player', isAI: false },
41-
{ id: 'ai' as const, name: 'AI', isAI: true }
42-
],
43-
currentPlayerId: 'player1' as const,
44-
status: 'playing' as const,
45-
createdAt: new Date('2024-01-01T10:00:00Z'),
46-
updatedAt: new Date('2024-01-01T10:00:00Z'),
47-
rounds: [],
48-
currentRound: 0,
49-
scores: {
50-
player1: 0,
51-
ai: 0
52-
},
53-
maxRounds: 3
54-
});
55-
37+
// Use shared test data factory to reduce duplication
5638
let mockGameState: RPSGameState;
5739
let mockGameSession: GameSession<RPSGameState>;
5840

5941
beforeEach(() => {
6042
vi.clearAllMocks();
6143

62-
// Create fresh mock state for each test
63-
mockGameState = createMockGameState();
44+
// Create fresh test data for each test using shared factory
45+
mockGameState = createRPSTestState();
6446
mockGameSession = {
6547
gameState: mockGameState,
6648
gameType: 'rock-paper-scissors' as const,

web/src/app/api/games/tic-tac-toe/route.test.ts

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { vi } from 'vitest'
22
import { NextRequest } from 'next/server';
33
import type { GameSession, TicTacToeGameState } from '@turn-based-mcp/shared';
4+
import { createTicTacToeTestState } from '../../../../test-utils/common-test-data';
45

56
// Use vi.hoisted() to ensure the mock object is available during hoisting
67
const mockGame = vi.hoisted(() => ({
@@ -33,27 +34,8 @@ import { GET, POST } from './route';
3334
const mockGameStorage = vi.mocked(gameStorage);
3435

3536
describe('/api/games/tic-tac-toe', () => {
36-
// Create the mock game state at module level
37-
const mockGameState: TicTacToeGameState = {
38-
id: 'test-game-1',
39-
players: [
40-
{ id: 'player1' as const, name: 'Player', isAI: false },
41-
{ id: 'ai' as const, name: 'AI', isAI: true }
42-
],
43-
currentPlayerId: 'player1' as const,
44-
status: 'playing' as const,
45-
createdAt: new Date('2024-01-01T10:00:00Z'),
46-
updatedAt: new Date('2024-01-01T10:00:00Z'),
47-
board: [
48-
[null, null, null],
49-
[null, null, null],
50-
[null, null, null]
51-
],
52-
playerSymbols: {
53-
player1: 'X' as const,
54-
ai: 'O' as const
55-
}
56-
};
37+
// Use shared test data factory to reduce duplication
38+
const mockGameState = createTicTacToeTestState();
5739

5840
const mockGameSession: GameSession<TicTacToeGameState> = {
5941
gameState: mockGameState,
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* API Route Testing Utilities for Web Package
3+
*
4+
* Provides web-specific mock patterns and utilities for testing
5+
* Next.js API routes consistently across all game types.
6+
*/
7+
8+
import { vi } from 'vitest'
9+
import {
10+
createSharedGameMocks,
11+
createStorageMocks,
12+
createMockTicTacToeGameState,
13+
createMockRPSGameState,
14+
createMockGameSession
15+
} from '@turn-based-mcp/shared'
16+
17+
/**
18+
* Setup standard API route mocks for a specific game type
19+
* This consolidates the common mock setup pattern used across API route tests
20+
*/
21+
export function setupAPIRouteMocks(gameType: 'tic-tac-toe' | 'rock-paper-scissors') {
22+
const gameClass = gameType === 'tic-tac-toe' ? 'TicTacToeGame' : 'RockPaperScissorsGame'
23+
24+
// Create shared game mocks
25+
const { mockImplementation, mockGame } = createSharedGameMocks(gameClass)
26+
27+
// Create storage mocks
28+
const storageMocks = createStorageMocks(gameType)
29+
30+
// Setup vi.mock calls
31+
vi.mock('@turn-based-mcp/shared', () => mockImplementation)
32+
vi.mock('../../../../lib/game-storage', () => storageMocks)
33+
34+
return {
35+
mockGame,
36+
storageMocks
37+
}
38+
}
39+
40+
/**
41+
* Setup mocks for move route testing
42+
* Handles the slightly different mock setup needed for move routes
43+
*/
44+
export function setupMoveRouteMocks(gameType: 'tic-tac-toe' | 'rock-paper-scissors') {
45+
const gameClass = gameType === 'tic-tac-toe' ? 'TicTacToeGame' : 'RockPaperScissorsGame'
46+
47+
// Create game mock
48+
const mockGame = {
49+
getInitialState: vi.fn(),
50+
validateMove: vi.fn(),
51+
applyMove: vi.fn(),
52+
checkGameEnd: vi.fn(),
53+
getValidMoves: vi.fn()
54+
}
55+
56+
// Setup shared mock
57+
vi.mock('@turn-based-mcp/shared', () => ({
58+
...vi.importActual('@turn-based-mcp/shared'),
59+
[gameClass]: vi.fn(() => mockGame),
60+
__mockGameInstance: mockGame
61+
}))
62+
63+
// Storage mocks - different path depth for move routes
64+
if (gameType === 'tic-tac-toe') {
65+
vi.mock('../../../../../../lib/game-storage', () => ({
66+
getTicTacToeGame: vi.fn(),
67+
setTicTacToeGame: vi.fn()
68+
}))
69+
} else {
70+
vi.mock('../../../../../../lib/game-storage', () => ({
71+
getRPSGame: vi.fn(),
72+
setRPSGame: vi.fn()
73+
}))
74+
}
75+
76+
return mockGame
77+
}
78+
79+
/**
80+
* Create standard mock game sessions for testing
81+
*/
82+
export function createTestGameSessions() {
83+
return {
84+
ticTacToe: createMockGameSession(
85+
createMockTicTacToeGameState(),
86+
'tic-tac-toe'
87+
),
88+
rockPaperScissors: createMockGameSession(
89+
createMockRPSGameState(),
90+
'rock-paper-scissors'
91+
)
92+
}
93+
}
94+
95+
/**
96+
* Helper to create NextRequest mock for testing
97+
*/
98+
export function createMockNextRequest(url: string, init?: { method?: string; body?: any }) {
99+
const mockRequest = {
100+
url,
101+
method: init?.method || 'GET',
102+
json: vi.fn().mockResolvedValue(init?.body || {})
103+
}
104+
105+
return mockRequest as any
106+
}

0 commit comments

Comments
 (0)