JavaScript テスト入門

JavaScriptアプリケーションのテスト手法について、単体テストから統合テストまで基本的な考え方を解説します。

|

JavaScript テスト入門

品質の高いコードを書くために不可欠なテストの基本を学びましょう。

単体テストの基本

// math.js
export function add(a, b) {
  return a + b
}

export function multiply(a, b) {
  return a * b
}

export function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero')
  }
  return a / b
}
// math.test.js
import { add, multiply, divide } from './math.js'

describe('Math functions', () => {
  test('add should return sum of two numbers', () => {
    expect(add(2, 3)).toBe(5)
    expect(add(-1, 1)).toBe(0)
    expect(add(0, 0)).toBe(0)
  })

  test('multiply should return product of two numbers', () => {
    expect(multiply(3, 4)).toBe(12)
    expect(multiply(-2, 3)).toBe(-6)
    expect(multiply(0, 5)).toBe(0)
  })

  test('divide should return quotient of two numbers', () => {
    expect(divide(10, 2)).toBe(5)
    expect(divide(7, 2)).toBe(3.5)
  })

  test('divide should throw error when dividing by zero', () => {
    expect(() => divide(5, 0)).toThrow('Division by zero')
  })
})

非同期コードのテスト

// api.js
export async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`)
  
  if (!response.ok) {
    throw new Error('User not found')
  }
  
  return response.json()
}
// api.test.js
import { fetchUser } from './api.js'

// fetchをモック
global.fetch = jest.fn()

describe('fetchUser', () => {
  beforeEach(() => {
    fetch.mockClear()
  })

  test('should return user data when API call succeeds', async () => {
    const mockUser = { id: 1, name: 'John Doe' }
    
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser
    })

    const user = await fetchUser(1)
    
    expect(fetch).toHaveBeenCalledWith('/api/users/1')
    expect(user).toEqual(mockUser)
  })

  test('should throw error when API call fails', async () => {
    fetch.mockResolvedValueOnce({
      ok: false
    })

    await expect(fetchUser(999)).rejects.toThrow('User not found')
  })
})

Reactコンポーネントのテスト

// Button.jsx
export function Button({ onClick, disabled, children }) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {children}
    </button>
  )
}
// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'

describe('Button component', () => {
  test('renders button with text', () => {
    render(<Button>Click me</Button>)
    
    expect(screen.getByRole('button')).toBeInTheDocument()
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  test('calls onClick when clicked', () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    
    fireEvent.click(screen.getByRole('button'))
    
    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  test('is disabled when disabled prop is true', () => {
    render(<Button disabled>Click me</Button>)
    
    expect(screen.getByRole('button')).toBeDisabled()
  })
})

テスト設定(Jest)

// jest.config.js
export default {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  moduleNameMapping: {
    '\\.(css|less|scss)$': 'identity-obj-proxy'
  },
  collectCoverageFrom: [
    'src/**/*.{js,jsx}',
    '!src/index.js',
    '!src/setupTests.js'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
}
// src/setupTests.js
import '@testing-library/jest-dom'

テストを書くことで、バグの早期発見とコードの品質向上を実現できます。