본문으로 건너뛰기

파일 기반 라우팅으로 라우터를 테스트하는 방법

이 가이드에서는 파일 기반 라우팅을 사용하는 TanStack Router 애플리케이션의 테스트 방법을 설명하며, 라우트 생성, 파일 기반 라우트 컴포넌트, 파일 기반 라우팅 패턴 테스트를 포함합니다.

빠른 시작​

라우트 모킹 유틸리티를 설정하고, 생성된 라우트 트리를 테스트하고, 파일 기반 라우트 구조와 규칙에 맞는 패턴을 구현하여 파일 기반 라우팅을 테스트합니다.


파일 기반 라우팅 테스트 이해​

파일 기반 라우팅 테스트는 다음과 같은 몇 가지 주요 측면에서 코드 기반 라우팅 테스트와 다릅니다.

  • 생성된 라우트 트리: 파일 시스템 구조에서 라우트가 자동으로 생성됩니다.
  • 파일 규칙: 라우트는 특정 파일 이름 규칙(index.tsx, route.tsx, $param.tsx)을 따릅니다.
  • 라우트 검색: 명시적 import가 아니라 파일 시스템 스캔을 통해 라우트를 검색합니다.
  • 타입 생성: 라우트 타입이 자동으로 생성되므로 특별한 테스트 고려 사항이 필요합니다.

파일 기반 라우트 테스트 설정​

1. 테스트 종속성 설치​

파일 기반 라우팅 테스트에는 일반 라우터 테스트와 동일한 기본 종속성이 필요합니다.

npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom

2. 테스트 환경 구성​

파일 기반 라우팅을 지원하도록 vitest.config.ts를 만듭니다.

import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
plugins: [
tanstackRouter({
// Configure for test environment
routesDirectory: './src/routes',
generatedRouteTree: './src/routeTree.gen.ts',
disableLogging: true,
}),
react(),
],
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
typecheck: { enabled: true },
watch: false,
// Ensure route tree is generated before tests
globals: true,
},
})

3. 라우트 테스트 유틸리티 만들기​

src/test/file-route-utils.tsx를 만듭니다.

import React from 'react'
import { render, RenderOptions } from '@testing-library/react'
import {
createRouter,
RouterProvider,
createMemoryHistory,
} from '@tanstack/react-router'

// Import the generated route tree
import { routeTree } from '../routeTree.gen'

// Create test router with generated route tree
export function createTestRouterFromFiles(initialLocation = '/') {
const router = createRouter({
routeTree,
history: createMemoryHistory({
initialEntries: [initialLocation],
}),
context: {
// Add any required context for your routes
},
})

return router
}

// Custom render function for file-based routes
interface RenderWithFileRoutesOptions extends Omit<RenderOptions, 'wrapper'> {
initialLocation?: string
routerContext?: any
}

export function renderWithFileRoutes(
ui: React.ReactElement,
{
initialLocation = '/',
routerContext = {},
...renderOptions
}: RenderWithFileRoutesOptions = {},
) {
const router = createRouter({
routeTree,
history: createMemoryHistory({
initialEntries: [initialLocation],
}),
context: routerContext,
})

function Wrapper({ children }: { children: React.ReactNode }) {
return <RouterProvider router={router}>{children}</RouterProvider>
}

return {
...render(ui, { wrapper: Wrapper, ...renderOptions }),
router,
}
}

// Helper to test specific file routes
export function createMockFileRoute(
path: string,
component: React.ComponentType,
) {
// This is useful for isolated testing when you don't want to use the full route tree
return {
path,
component,
// Add other common route properties as needed
}
}

파일 기반 라우트 구조 테스트​

1. 라우트 트리 생성 테스트​

import { describe, it, expect } from 'vitest'
import { routeTree } from '../routeTree.gen'

describe('Generated Route Tree', () => {
it('should generate route tree from file structure', () => {
// Test that route tree exists and has expected structure
expect(routeTree).toBeDefined()
expect(routeTree.children).toBeDefined()
})

it('should include all expected routes', () => {
// Get all route paths from the generated tree
const getAllRoutePaths = (tree: any, paths: string[] = []): string[] => {
if (tree.path) {
paths.push(tree.path)
}
if (tree.children) {
tree.children.forEach((child: any) => {
getAllRoutePaths(child, paths)
})
}
return paths
}

const routePaths = getAllRoutePaths(routeTree)

// Test that expected routes are present
expect(routePaths).toContain('/')
expect(routePaths).toContain('/about')
// Add assertions for your specific routes
})

it('should have correct route hierarchy', () => {
// Test parent-child relationships
const homeRoute = routeTree.children?.find(
(child: any) => child.path === '/',
)
expect(homeRoute).toBeDefined()

// Test for specific route structure based on your file organization
// For example, if you have /posts/$postId routes:
// const postsRoute = routeTree.children?.find((child: any) => child.path === '/posts')
// expect(postsRoute?.children).toBeDefined()
})
})

2. 파일 라우트 규칙 테스트​

import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithFileRoutes } from '../test/file-route-utils'

describe('File Route Conventions', () => {
it('should render index route at root path', () => {
renderWithFileRoutes(<div />, {
initialLocation: '/',
})

// Test that the index route component renders
// This depends on what your src/routes/index.tsx exports
expect(screen.getByText('Welcome Home!')).toBeInTheDocument()
})

it('should handle route parameters from filename', () => {
// If you have a route like src/routes/posts/$postId.tsx
renderWithFileRoutes(<div />, {
initialLocation: '/posts/123',
})

// Test that parameter is correctly parsed from file-based route
expect(screen.getByText(/Post.*123/)).toBeInTheDocument()
})

it('should handle nested routes from directory structure', () => {
// If you have src/routes/dashboard/settings.tsx
renderWithFileRoutes(<div />, {
initialLocation: '/dashboard/settings',
})

expect(screen.getByText(/Settings/)).toBeInTheDocument()
})

it('should handle layout routes', () => {
// If you have src/routes/_layout.tsx
renderWithFileRoutes(<div />, {
initialLocation: '/some-nested-route',
})

// Test that layout is rendered for nested routes
expect(screen.getByTestId('layout-header')).toBeInTheDocument()
})
})

파일 기반 라우트 컴포넌트 테스트​

1. 개별 라우트 파일 테스트​

import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import { createFileRoute } from '@tanstack/react-router'
import { renderWithFileRoutes } from '../test/file-route-utils'

describe('Individual Route Components', () => {
it('should test about route component', () => {
renderWithFileRoutes(<div />, {
initialLocation: '/about',
})

expect(screen.getByText('About')).toBeInTheDocument()
})

it('should test route with loader data', () => {
// For a route like src/routes/posts/index.tsx with loader
renderWithFileRoutes(<div />, {
initialLocation: '/posts',
})

// Wait for loader data to load
expect(screen.getByText(/Posts List/)).toBeInTheDocument()
})

it('should test route with search params validation', () => {
// For a route with validateSearch in src/routes/search.tsx
renderWithFileRoutes(<div />, {
initialLocation: '/search?q=react&page=1',
})

expect(screen.getByDisplayValue('react')).toBeInTheDocument()
})
})

2. 라우트별 훅 테스트​

import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithFileRoutes } from '../test/file-route-utils'

describe('Route-Specific Hooks', () => {
it('should test useParams in parameterized route', () => {
// Create a test component that uses Route.useParams()
function TestComponent() {
// This would be available in the actual route component
const params = Route.useParams()
return <div data-testid="param-value">{params.postId}</div>
}

renderWithFileRoutes(<TestComponent />, {
initialLocation: '/posts/abc123',
})

expect(screen.getByTestId('param-value')).toHaveTextContent('abc123')
})

it('should test useLoaderData in route with loader', () => {
renderWithFileRoutes(<div />, {
initialLocation: '/posts/123',
})

// Test that loader data is available in the component
expect(screen.getByText(/Post Title/)).toBeInTheDocument()
})

it('should test useSearch in route with search validation', () => {
renderWithFileRoutes(<div />, {
initialLocation: '/search?q=typescript&sort=date',
})

// Test that search params are correctly parsed
expect(screen.getByDisplayValue('typescript')).toBeInTheDocument()
expect(screen.getByText(/sorted by date/)).toBeInTheDocument()
})
})

파일 기반 라우트로 라우트 탐색 테스트​

import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { renderWithFileRoutes } from '../test/file-route-utils'

describe('File-Based Route Navigation', () => {
it('should navigate between file-based routes', async () => {
const user = userEvent.setup()

const { router } = renderWithFileRoutes(<div />, {
initialLocation: '/',
})

// Initial state - should be on home route
expect(screen.getByText('Welcome Home!')).toBeInTheDocument()
expect(router.state.location.pathname).toBe('/')

// Click navigation link
await user.click(screen.getByRole('link', { name: /about/i }))

// Should navigate to about route
expect(screen.getByText('About')).toBeInTheDocument()
expect(router.state.location.pathname).toBe('/about')
})

it('should handle dynamic route navigation', async () => {
const user = userEvent.setup()

renderWithFileRoutes(<div />, {
initialLocation: '/posts',
})

// Click on a post link (assuming your posts route renders links)
await user.click(screen.getByRole('link', { name: /View Post 1/i }))

// Should navigate to dynamic post route
expect(screen.getByText(/Post 1 Details/)).toBeInTheDocument()
})

it('should handle nested route navigation', async () => {
const user = userEvent.setup()

renderWithFileRoutes(<div />, {
initialLocation: '/dashboard',
})

// Navigate to nested route
await user.click(screen.getByRole('link', { name: /settings/i }))

expect(screen.getByText(/Dashboard Settings/)).toBeInTheDocument()
})
})

2. 프로그래밍 방식 탐색 테스트​

import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { renderWithFileRoutes } from '../test/file-route-utils'

describe('Programmatic Navigation', () => {
it('should programmatically navigate between file routes', async () => {
const user = userEvent.setup()

const { router } = renderWithFileRoutes(<div />, {
initialLocation: '/',
})

// Trigger programmatic navigation (button in your component)
await user.click(screen.getByRole('button', { name: /Go to Posts/i }))

expect(router.state.location.pathname).toBe('/posts')
})

it('should navigate with search params', async () => {
const user = userEvent.setup()

const { router } = renderWithFileRoutes(<div />, {
initialLocation: '/search',
})

// Trigger search with params
await user.type(screen.getByRole('textbox'), 'test query')
await user.click(screen.getByRole('button', { name: /search/i }))

expect(router.state.location.search).toMatchObject({
q: 'test query',
})
})
})

파일 기반 라우트 가드 및 로더 테스트​

1. 라우트 가드 테스트​

import { describe, it, expect, vi } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithFileRoutes } from '../test/file-route-utils'

describe('File-Based Route Guards', () => {
it('should redirect unauthenticated users from protected routes', () => {
// Mock unauthenticated state
const mockAuth = { isAuthenticated: false, user: null }

renderWithFileRoutes(<div />, {
initialLocation: '/dashboard',
routerContext: { auth: mockAuth },
})

// Should redirect to login (based on your beforeLoad implementation)
expect(screen.getByText(/Please log in/)).toBeInTheDocument()
})

it('should allow authenticated users to access protected routes', () => {
const mockAuth = {
isAuthenticated: true,
user: { id: '1', name: 'John' },
}

renderWithFileRoutes(<div />, {
initialLocation: '/dashboard',
routerContext: { auth: mockAuth },
})

expect(screen.getByText(/Welcome to Dashboard/)).toBeInTheDocument()
})
})

2. 라우트 로더 테스트​

import { describe, it, expect, vi } from 'vitest'
import { screen, waitFor } from '@testing-library/react'
import { renderWithFileRoutes } from '../test/file-route-utils'

describe('File-Based Route Loaders', () => {
it('should load data for route with loader', async () => {
// Mock the API function used in your route loader
const mockFetchPost = vi.fn().mockResolvedValue({
id: '123',
title: 'Test Post',
content: 'Test content',
})

// If your route loader uses a global API function, mock it
vi.mock('../api/posts', () => ({
fetchPost: mockFetchPost,
}))

renderWithFileRoutes(<div />, {
initialLocation: '/posts/123',
})

await waitFor(() => {
expect(screen.getByText('Test Post')).toBeInTheDocument()
})

expect(mockFetchPost).toHaveBeenCalledWith('123')
})

it('should handle loader errors', async () => {
const mockFetchPost = vi.fn().mockRejectedValue(new Error('Post not found'))

vi.mock('../api/posts', () => ({
fetchPost: mockFetchPost,
}))

renderWithFileRoutes(<div />, {
initialLocation: '/posts/invalid',
})

await waitFor(() => {
expect(screen.getByText(/Error.*Post not found/)).toBeInTheDocument()
})
})
})

파일 라우트 검증 테스트​

1. 검색 매개변수 검증 테스트​

import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithFileRoutes } from '../test/file-route-utils'

describe('File Route Validation', () => {
it('should validate search parameters', () => {
// Test with valid search params
renderWithFileRoutes(<div />, {
initialLocation: '/search?q=react&page=1&sort=date',
})

expect(screen.getByDisplayValue('react')).toBeInTheDocument()
expect(screen.getByText(/Page 1/)).toBeInTheDocument()
})

it('should handle invalid search parameters', () => {
// Test with invalid search params (e.g., invalid page number)
renderWithFileRoutes(<div />, {
initialLocation: '/search?page=invalid&sort=unknown',
})

// Should fall back to defaults based on your validation schema
expect(screen.getByText(/Page 1/)).toBeInTheDocument() // default page
})

it('should validate route parameters', () => {
// Test with valid route param
renderWithFileRoutes(<div />, {
initialLocation: '/posts/123',
})

expect(screen.getByText(/Post 123/)).toBeInTheDocument()
})
})

파일 라우트 오류 경계 테스트​

1. 라우트 수준 오류 처리 테스트​

import { describe, it, expect, vi } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithFileRoutes } from '../test/file-route-utils'

describe('File Route Error Handling', () => {
it('should handle component errors with error boundary', () => {
// Mock console.error to avoid noise in test output
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

// Force an error in a route component
vi.mock('../routes/error-prone.tsx', () => ({
Route: {
component: () => {
throw new Error('Test error')
},
},
}))

renderWithFileRoutes(<div />, {
initialLocation: '/error-prone',
})

expect(screen.getByText(/Something went wrong/)).toBeInTheDocument()

consoleSpy.mockRestore()
})

it('should handle loader errors with error component', async () => {
const mockFailingLoader = vi
.fn()
.mockRejectedValue(new Error('Load failed'))

vi.mock('../api/data', () => ({
loadData: mockFailingLoader,
}))

renderWithFileRoutes(<div />, {
initialLocation: '/data-route',
})

expect(screen.getByText(/Failed to load data/)).toBeInTheDocument()
})
})

생성된 라우트 타입을 사용한 테스트​

1. 타입 안전성 테스트​

import { describe, it, expect } from 'vitest'
import { useNavigate } from '@tanstack/react-router'
import { renderWithFileRoutes } from '../test/file-route-utils'

describe('Generated Route Types', () => {
it('should provide type-safe navigation', () => {
function TestComponent() {
const navigate = useNavigate()

const handleNavigate = () => {
// This should be type-safe based on your generated routes
navigate({
to: '/posts/$postId',
params: { postId: '123' },
search: { tab: 'comments' },
})
}

return (
<button onClick={handleNavigate} data-testid="navigate-btn">
Navigate
</button>
)
}

const { router } = renderWithFileRoutes(<TestComponent />, {
initialLocation: '/',
})

// Test the navigation works correctly
const button = screen.getByTestId('navigate-btn')
fireEvent.click(button)

expect(router.state.location.pathname).toBe('/posts/123')
expect(router.state.location.search).toEqual({ tab: 'comments' })
})
})

라우트 트리 변경 테스트​

1. 개발 중 라우트 생성 테스트​

import { describe, it, expect } from 'vitest'
import { routeTree } from '../routeTree.gen'

describe('Route Tree Development', () => {
it('should regenerate routes when files change', () => {
// This test ensures your route tree is properly generated
// You can add specific assertions based on your file structure

expect(routeTree).toBeDefined()
expect(typeof routeTree.children).toBe('object')

// Test specific routes exist
const routes = getAllRouteIds(routeTree)
expect(routes).toContain('/')
expect(routes).toContain('/about')
// Add assertions for your specific routes
})

// Helper function to get all route IDs from tree
function getAllRouteIds(tree: any, ids: string[] = []): string[] {
if (tree.id) {
ids.push(tree.id)
}
if (tree.children) {
Object.values(tree.children).forEach((child: any) => {
getAllRouteIds(child, ids)
})
}
return ids
}
})

파일 기반 라우트의 E2E 테스트​

1. 파일 기반 라우트를 위한 Playwright 구성​

e2e/file-routing.spec.ts를 만듭니다.

import { test, expect } from '@playwright/test'

test.describe('File-Based Route E2E', () => {
test('should navigate through file-based route structure', async ({
page,
}) => {
await page.goto('/')

// Test home route (from src/routes/index.tsx)
await expect(page.locator('h3')).toContainText('Welcome Home!')

// Navigate to about route (from src/routes/about.tsx)
await page.click('text=About')
await expect(page).toHaveURL('/about')
await expect(page.locator('h3')).toContainText('About')

// Test browser navigation
await page.goBack()
await expect(page).toHaveURL('/')
})

test('should handle dynamic routes from file structure', async ({ page }) => {
await page.goto('/posts')

// Click on a dynamic post link (from src/routes/posts/$postId.tsx)
await page.click('[data-testid="post-link-1"]')
await expect(page).toHaveURL('/posts/1')
await expect(page.locator('h1')).toContainText('Post 1')
})

test('should handle nested routes', async ({ page }) => {
await page.goto('/dashboard')

// Navigate to nested route (from src/routes/dashboard/settings.tsx)
await page.click('text=Settings')
await expect(page).toHaveURL('/dashboard/settings')
await expect(page.locator('h2')).toContainText('Settings')
})
})

일반적인 파일 기반 라우팅 테스트 패턴​

1. 테스트를 위한 라우트 파일 모킹​

// src/test/mock-file-routes.tsx
import { createFileRoute } from '@tanstack/react-router'

// Mock individual route for isolated testing
export const createMockFileRoute = (
path: string,
component: React.ComponentType,
options: any = {},
) => {
return createFileRoute(path)({
component,
...options,
})
}

// Common test route components
export const TestHomeRoute = createMockFileRoute('/', () => (
<div data-testid="home">Home Page</div>
))

export const TestAboutRoute = createMockFileRoute('/about', () => (
<div data-testid="about">About Page</div>
))

export const TestDynamicRoute = createMockFileRoute('/posts/$postId', () => {
const { postId } = Route.useParams()
return <div data-testid="post">Post {postId}</div>
})

2. 라우트 검색 테스트​

import { describe, it, expect } from 'vitest'

describe('Route Discovery', () => {
it('should discover all routes from file structure', () => {
// Test that your route tree includes all expected routes
// This helps catch when routes are accidentally not being generated

const expectedRoutes = [
'/',
'/about',
'/posts',
'/posts/$postId',
'/dashboard',
'/dashboard/settings',
]

expectedRoutes.forEach((routePath) => {
const routeExists = checkRouteExists(routeTree, routePath)
expect(routeExists).toBe(true)
})
})
})

function checkRouteExists(tree: any, path: string): boolean {
// Implementation to check if route exists in tree
// This depends on your route tree structure
return true // Simplified
}

파일 기반 라우트 테스트 모범 사례​

1. 테스트 구성​

src/
├── routes/
│ ├── __root.tsx
│ ├── index.tsx
│ ├── about.tsx
│ ├── posts/
│ │ ├── index.tsx
│ │ └── $postId.tsx
├── test/
│ ├── setup.ts
│ ├── file-route-utils.tsx
│ └── routes/
│ ├── index.test.tsx
│ ├── about.test.tsx
│ └── posts/
│ ├── index.test.tsx
│ └── $postId.test.tsx

2. 일반적인 테스트 패턴​

// Test file for each route file
describe('Posts Route (/posts)', () => {
it('should render posts list', () => {
renderWithFileRoutes(<div />, {
initialLocation: '/posts',
})

expect(screen.getByText(/Posts/)).toBeInTheDocument()
})

it('should handle loading state', () => {
// Test pending state for route with loader
})

it('should handle error state', () => {
// Test error handling for route
})
})

// Test route groups
describe('Dashboard Routes', () => {
describe('/dashboard', () => {
// Dashboard index tests
})

describe('/dashboard/settings', () => {
// Settings route tests
})
})

파일 기반 라우트 테스트 문제 해결​

일반적인 문제​

문제: 테스트에서 라우트 트리를 찾을 수 없습니다.

Error: Cannot find module '../routeTree.gen'

해결 방법: 테스트 설정에서 라우트 트리가 생성되는지 확인합니다.

// vitest.config.ts
export default defineConfig({
plugins: [
tanstackRouter(), // Ensure this runs before tests
react(),
],
test: {
setupFiles: ['./src/test/setup.ts'],
},
})

문제: 파일을 변경한 후 테스트에서 라우트가 업데이트되지 않습니다.

해결 방법: 테스트 설정에서 모듈 캐시를 지웁니다.

// src/test/setup.ts
beforeEach(() => {
vi.clearAllMocks()
// Clear route tree cache if needed
delete require.cache[require.resolve('../routeTree.gen')]
})

문제: 생성된 라우트를 사용하는 테스트에서 타입 오류가 발생합니다.

해결 방법: 올바른 TypeScript 구성이 적용되었는지 확인합니다.

{
"compilerOptions": {
"types": ["vitest/globals", "@testing-library/jest-dom"],
"moduleResolution": "bundler"
},
"include": ["src/**/*", "src/routeTree.gen.ts"]
}

다음 단계​

파일 기반 라우트 테스트를 설정한 후에는 다음 작업을 고려할 수 있습니다.