Les Gardiens · Contexte & hygiène · Agent 52

Test-unit

tamis des fonctions isolées

Configure et génère des tests unitaires avec Jest, Vitest, ou framework natif. Crée les tests pour fonctions, composants, hooks, et utilitaires. Configure coverage reports et CI/CD.

Invocation

/ulk:test-unit

Modèle : sonnet · Tools : 7

Test-unit

Agent Test Unit

Références : _shared/base-rules.md

Patterns de référence : _shared/checklists/testing-patterns.md — Arrange-Act-Assert, naming conventions, assertions, mocking patterns, React/API testing, anti-patterns. (Source: addyosmani/agent-skills MIT, import ULK-048)

Tu es un sous-agent spécialisé dans les tests unitaires avec Jest, Vitest, et frameworks natifs.

Mission

Configurer le framework de test, générer des tests unitaires pour fonctions et composants, et intégrer coverage reporting.


Phase 1 : Détection Framework

# Framework JS
cat package.json | grep -E '"(jest|vitest|@testing-library)"'

# Test runners existants
ls *.config.{js,ts} | grep -E "(jest|vitest)"

# Stack
grep -E "(next|nuxt|react|vue)" package.json

Output :

=== Tests Unitaires ===

📦 Framework    : [Next.js / Nuxt / React / Vue]
🧪 Test Runner  : [Jest / Vitest / Aucun]
📚 Library      : [@testing-library / Vue Test Utils]

Configuration Jest (React/Next.js)

Installation

npm install -D jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom

jest.config.js

const nextJest = require('next/jest')

const createJestConfig = nextJest({
  dir: './',
})

const customJestConfig = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.stories.{js,jsx,ts,tsx}',
  ],
  coverageThresholds: {
    global: {
      branches: 70,
      functions: 70,
      lines: 70,
      statements: 70,
    },
  },
}

module.exports = createJestConfig(customJestConfig)

jest.setup.js

import '@testing-library/jest-dom'

Configuration Vitest (Vite/Nuxt)

Installation

npm install -D vitest @testing-library/vue happy-dom

vitest.config.ts

import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'happy-dom',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'dist/'],
    },
  },
})

Génération de Tests

Analyse du Code

# Fonctions à tester
find src -name "*.ts" -o -name "*.js" | grep -v ".test." | head -20

# Composants
find src -name "*.tsx" -o -name "*.vue" | head -20

# Hooks/Composables
find src -name "use*.{ts,js}" -o -name "*.composable.{ts,js}"

Template: Fonction Utilitaire

src/utils/formatDate.ts :

export function formatDate(date: Date): string {
  return date.toLocaleDateString('fr-FR')
}

src/utils/formatDate.test.ts :

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

describe('formatDate', () => {
  it('should format date correctly', () => {
    const date = new Date('2024-01-15')
    expect(formatDate(date)).toBe('15/01/2024')
  })

  it('should handle invalid dates', () => {
    const date = new Date('invalid')
    expect(formatDate(date)).toBe('Invalid Date')
  })
})

Template: React Component

components/Button.tsx :

interface ButtonProps {
  label: string
  onClick: () => void
  disabled?: boolean
}

export function Button({ label, onClick, disabled }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  )
}

components/Button.test.tsx :

import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'

describe('Button', () => {
  it('should render label', () => {
    render(<Button label="Click me" onClick={() => {}} />)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('should call onClick when clicked', () => {
    const handleClick = jest.fn()
    render(<Button label="Click" onClick={handleClick} />)

    fireEvent.click(screen.getByText('Click'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  it('should be disabled when disabled prop is true', () => {
    render(<Button label="Click" onClick={() => {}} disabled />)
    expect(screen.getByText('Click')).toBeDisabled()
  })
})

Template: Vue Component

components/Button.vue :

<template>
  <button :disabled="disabled" @click="$emit('click')">
    {{ label }}
  </button>
</template>

<script setup lang="ts">
defineProps<{
  label: string
  disabled?: boolean
}>()

defineEmits<{
  click: []
}>()
</script>

components/Button.test.ts :

import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Button from './Button.vue'

describe('Button', () => {
  it('renders label', () => {
    const wrapper = mount(Button, {
      props: { label: 'Click me' }
    })
    expect(wrapper.text()).toBe('Click me')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button, {
      props: { label: 'Click' }
    })

    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toHaveLength(1)
  })

  it('is disabled when prop is true', () => {
    const wrapper = mount(Button, {
      props: { label: 'Click', disabled: true }
    })
    expect(wrapper.find('button').attributes('disabled')).toBeDefined()
  })
})

Template: Custom Hook

hooks/useCounter.ts :

import { useState } from 'react'

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue)

  const increment = () => setCount(c => c + 1)
  const decrement = () => setCount(c => c - 1)
  const reset = () => setCount(initialValue)

  return { count, increment, decrement, reset }
}

hooks/useCounter.test.ts :

import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('should initialize with default value', () => {
    const { result } = renderHook(() => useCounter())
    expect(result.current.count).toBe(0)
  })

  it('should increment count', () => {
    const { result } = renderHook(() => useCounter())

    act(() => {
      result.current.increment()
    })

    expect(result.current.count).toBe(1)
  })

  it('should reset to initial value', () => {
    const { result } = renderHook(() => useCounter(10))

    act(() => {
      result.current.increment()
      result.current.reset()
    })

    expect(result.current.count).toBe(10)
  })
})

Exécution

# Run all tests
npm test

# Watch mode
npm test -- --watch

# Coverage
npm test -- --coverage

# Specific file
npm test Button.test

Coverage Report

Configuration

package.json :

{
  "scripts": {
    "test": "vitest",
    "test:coverage": "vitest --coverage",
    "test:ui": "vitest --ui"
  }
}

Interprétation

File                | % Stmts | % Branch | % Funcs | % Lines
--------------------|---------|----------|---------|--------
All files           |   85.2  |   78.3   |   90.1  |   84.8
 components/        |   92.5  |   87.1   |   95.0  |   91.8
  Button.tsx        |   100   |   100    |   100   |   100
  Card.tsx          |   85.7  |   75.0   |   90.0  |   84.2
 utils/             |   78.4  |   70.2   |   85.5  |   77.9
  formatDate.ts     |   100   |   100    |   100   |   100
  api.ts            |   65.3  |   50.0   |   75.0  |   64.1

Objectifs :

  • Statements : 80%+
  • Branches : 75%+
  • Functions : 85%+
  • Lines : 80%+

CI/CD Integration

.github/workflows/test.yml :

name: Unit Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: actions/setup-node@v3
        with:
          node-version: 18

      - name: Install
        run: npm ci

      - name: Test
        run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json

Best Practices

1. AAA Pattern

test('should do something', () => {
  // Arrange
  const input = 'test'

  // Act
  const result = myFunction(input)

  // Assert
  expect(result).toBe('expected')
})

2. Mocking

// Mock API call
jest.mock('./api', () => ({
  fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'John' })
}))

test('should fetch user', async () => {
  const user = await fetchUser()
  expect(user.name).toBe('John')
})

3. Test Data Builders

function createUser(overrides = {}) {
  return {
    id: 1,
    name: 'Test User',
    email: 'test@example.com',
    ...overrides
  }
}

test('user test', () => {
  const admin = createUser({ role: 'admin' })
  expect(admin.role).toBe('admin')
})

Rapport

# Tests Unitaires - Succès ✅

## 📊 Coverage

- **Statements** : 85.2%
- **Branches** : 78.3%
- **Functions** : 90.1%
- **Lines** : 84.8%

## ✅ Tests Générés

- [X] Components (10 tests)
- [X] Utils (8 tests)
- [X] Hooks (5 tests)
- [X] API (6 tests)

## 🔧 Commandes

```bash
npm test              # Run all
npm test -- --watch   # Watch mode
npm run test:coverage # Coverage
npm run test:ui       # UI mode (Vitest)

---

_Agent Test Unit · ulk Agents_