Claude
Skills
Sign in
Back

testing

Included with Lifetime
$97 forever

This skill provides Testing Library best practices for the fitness app. Use when writing component tests, API tests, or database tests. Covers user-centric testing patterns, query strategies, and async state handling.

Design

What this skill does


# Testing Best Practices

This skill provides guidance for writing user-centric tests using Vitest, Testing Library, and Nuxt Test Utils.

## Core Principles

**User-Centric Philosophy**: Test what users see and do, not implementation details.

**Never Test Implementation**:
- ❌ Don't access `wrapper.vm` or internal component state
- ❌ Don't test props directly
- ❌ Don't query by CSS selectors or test IDs
- ❌ Don't shallow mount components
- ❌ Never include skipped tests with it.skip

**Always Test Behavior**:
- ✅ Query by accessibility (roles, labels, text)
- ✅ Interact like a user (click, type, keyboard)
- ✅ Assert on what users see
- ✅ Full component rendering with dependencies
- ✅ Ensure it works on Mobile and Desktop

## Test Stack

### Component Tests

Use `@nuxt/test-utils/runtime` for Nuxt components:

```typescript
import { renderSuspended } from '@nuxt/test-utils/runtime'
import { screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, beforeEach } from 'vitest'
import MyComponent from './MyComponent.vue'

describe('MyComponent', () => {
  let user: ReturnType<typeof userEvent.setup>

  beforeEach(() => {
    user = userEvent.setup()
  })

  describe('form rendering', () => {
    it('should display all form fields', async () => {
      await renderSuspended(MyComponent)

      expect(screen.getByLabelText(/email/i)).toBeDefined()
      expect(screen.getByLabelText(/password/i)).toBeDefined()
      expect(screen.getByRole('button', { name: /submit/i })).toBeDefined()
    })
  })

  describe('form input', () => {
    it('should allow user to type in fields', async () => {
      await renderSuspended(MyComponent)

      const emailInput = screen.getByLabelText(/email/i) as HTMLInputElement
      await user.type(emailInput, '[email protected]')

      expect(emailInput.value).toBe('[email protected]')
    })
  })
})
```

**Key imports:**
- `renderSuspended` - Render component in full Nuxt environment
- `screen` - Query rendered output
- `userEvent` - Simulate realistic user interactions
- Test file naming: `*.nuxt.test.ts` for Nuxt components

**File Naming Convention:**
- Component tests: `*.nuxt.test.ts` (e.g., `Login.nuxt.test.ts`)
- Page tests: `*.nuxt.test.ts` (e.g., `login.nuxt.test.ts`)
- Server/API tests: `*.test.ts` (e.g., `index.get.test.ts`)
- Utility tests: `*.test.ts` (e.g., `format.test.ts`)
- E2E tests: `*.spec.ts` (e.g., `login.spec.ts`)

### API Tests

Test server routes using `$fetch`:

```typescript
import { describe, it, expect, beforeAll } from 'vitest'

describe('/api/workouts', () => {
  let authCookie: string

  beforeAll(async () => {
    // Setup test user session
    const response = await $fetch('/api/auth/login', {
      method: 'POST',
      body: { email: '[email protected]', password: 'password' }
    })
    authCookie = response.headers.get('set-cookie') || ''
  })

  it('should return user workouts', async () => {
    const workouts = await $fetch('/api/workouts', {
      headers: { cookie: authCookie }
    })

    expect(Array.isArray(workouts)).toBe(true)
    expect(workouts[0]).toHaveProperty('id')
    expect(workouts[0]).toHaveProperty('name')
  })

  it('should return 401 without auth', async () => {
    await expect($fetch('/api/workouts')).rejects.toThrow('401')
  })
})
```

**Key points:**
- Use `$fetch` within tests to call API routes
- Test authentication, validation, errors, and success cases
- Test file naming: `*.test.ts` for API tests

### Database Tests

Test query functions directly:

```typescript
import { describe, it, expect, beforeEach } from 'vitest'
import { db } from '~~/server/database'
import { queryUserWorkouts } from '~~/server/database/queries/workouts'
import { users } from '~~/server/database/schema'

describe('queryUserWorkouts', () => {
  let userId: string

  beforeEach(async () => {
    // Create test user
    const [user] = await db.insert(users).values({
      email: '[email protected]',
      name: 'Test User'
    }).returning()
    userId = user.id
  })

  it('should return empty array when user has no workouts', async () => {
    const workouts = await queryUserWorkouts(userId)
    expect(workouts).toEqual([])
  })

  it('should enforce RLS - only return user\'s workouts', async () => {
    // Create workout for another user
    const [otherUser] = await db.insert(users).values({
      email: '[email protected]',
      name: 'Other'
    }).returning()

    // Create workout for other user
    await db.insert(workouts).values({
      userId: otherUser.id,
      name: 'Other\'s Workout'
    })

    // Query should return empty for original user
    const userWorkouts = await queryUserWorkouts(userId)
    expect(userWorkouts).toEqual([])
  })
})
```

**Key points:**
- Test query functions in isolation
- Use transactions for test data
- Verify RLS policies work correctly
- Test file naming: `*.test.ts` for database tests

## Query Priority

Use queries in this order (from Testing Library docs):

1. **getByRole** - Preferred for interactive elements
```typescript
screen.getByRole('button', { name: /sign in/i })
screen.getByRole('link', { name: /forgot password/i })
screen.getByRole('heading', { name: /welcome/i })
screen.getByRole('textbox', { name: /email/i })
```

2. **getByLabelText** - Best for form inputs
```typescript
screen.getByLabelText(/email/i)
screen.getByLabelText(/password/i)
```

3. **getByPlaceholderText** - When label isn't available
```typescript
screen.getByPlaceholderText(/enter your email/i)
```

4. **getByText** - For non-interactive text
```typescript
screen.getByText(/welcome back/i)
screen.getByText(/successfully logged in/i)
```

5. **getByTestId** - Last resort only (avoid if possible)
```typescript
screen.getByTestId('submit-button') // Use getByRole instead
```

**Query variants:**
- `getBy*` - Throws if not found, throws if multiple
- `queryBy*` - Returns null if not found
- `findBy*` - Async, waits for element to appear

**Regex matching:**
Always use case-insensitive regex for text matching:
```typescript
// ✅ Good
screen.getByText(/sign in/i)
screen.getByRole('button', { name: /submit/i })

// ❌ Bad
screen.getByText('Sign In') // Breaks if text changes
```

## Common Test Patterns

### Pattern 1: Form Submission

Test user filling out and submitting a form:

```typescript
describe('login form', () => {
  let user: ReturnType<typeof userEvent.setup>

  beforeEach(() => {
    user = userEvent.setup()
  })

  it('should submit form with valid credentials', async () => {
    const navigateTo = vi.fn()
    vi.stubGlobal('navigateTo', navigateTo)

    await renderSuspended(LoginForm)

    // Fill form
    await user.type(screen.getByLabelText(/email/i), '[email protected]')
    await user.type(screen.getByLabelText(/password/i), 'password123')

    // Submit
    await user.click(screen.getByRole('button', { name: /sign in/i }))

    // Assert navigation
    await waitFor(() => {
      expect(navigateTo).toHaveBeenCalledWith('/dashboard')
    })
  })
})
```

### Pattern 2: Interactive Elements

Test buttons, toggles, and other interactive elements:

```typescript
describe('password visibility toggle', () => {
  it('should toggle password visibility', async () => {
    await renderSuspended(LoginForm)

    const passwordInput = screen.getByLabelText(/password/i) as HTMLInputElement
    const toggleButton = screen.getByRole('button', { name: /show password/i })

    expect(passwordInput.type).toBe('password')

    await user.click(toggleButton)
    expect(passwordInput.type).toBe('text')

    await user.click(toggleButton)
    expect(passwordInput.type).toBe('password')
  })
})
```

### Pattern 3: Conditional Rendering

Test components that show/hide based on state:

```typescript
it('should show success message after submission', async () => {
  await renderSuspended(ForgotPasswordForm)

  // Initially no success message
  expect(screen.queryByText(/check your email/i)).toBeNull()

  // Submi
Files: 1
Size: 21.0 KB
Complexity: 25/100
Category: Design

Related in Design