8
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Reactテスト実践】Integration Testを書いてみた — RHF + TanStack Query編

8
Posted at

Reactテスト実践 React Hook Form + TanStack Query のテストを書いてみた Part 3

image.png

このシリーズはフレッシャーFEがテストをゼロから学ぶ記録です。


はじめに

今回はいよいよ実務に直結するテストを書いていきます。でも実務のコードはシンプルなコンポーネントより複雑です。

  • フォームにはバリデーションがある
  • データはAPIから非同期で取得する
  • ローディング中・エラー時・成功時でUIが変わる

今回はそういった実務に直結する場面のテストを、React Hook Form(RHF)とTanStack Queryを使いながら書いていきます。

正直に言うと、この2つのテストは最初かなり詰まりました。「どうモックすればいいかわからない」「非同期の待ち方がわからない」という壁にぶつかりました。この記事ではその詰まりポイントも含めて丁寧に解説します。

この記事を読み終えると:

  • RHFを使ったフォームのバリデーションテストが書ける
  • vi.mock でAPIをモックする方法がわかる
  • TanStack Queryのローディング・エラー・成功状態をテストできる
  • フォームsubmit → API呼び出し → 結果表示の一連の流れをテストできる

追加パッケージのインストール

npm install react-hook-form @tanstack/react-query
npm install -D msw@latest

今回新しく登場するのが MSW(Mock Service Worker) です。

MSWとは? なぜvi.mockより優れているのか

APIのモックには大きく2つのアプローチがあります。

アプローチ1:vi.mock でfetchをモックする

// fetchそのものをモックする方法
vi.mock('node-fetch')
global.fetch = vi.fn().mockResolvedValue({
  json: () => Promise.resolve({ id: 1, name: '田中' })
})

これでも動きますが、問題があります。fetch の実装の詳細に依存しているため、実際のHTTPリクエストとは動作が微妙に異なります。

アプローチ2:MSWでネットワーク層をモックする

// MSWはネットワーク層でリクエストを傍受する
http.get('/api/users', () => {
  return HttpResponse.json({ id: 1, name: '田中' })
})

MSWはブラウザやNode.jsのネットワーク層でリクエストを傍受します。コードは本物のfetchを使い、レスポンスだけを差し替えます。これにより実際の動作により近いテストが書けます。

MSWのセットアップ

src/test/server.ts を作成:

import { setupServer } from 'msw/node'

// テスト用のサーバーを作成(ハンドラーは各テストで追加する)
export const server = setupServer()

src/test/setup.ts を更新:

import '@testing-library/jest-dom'
import { server } from './server'

// 全テストの前にサーバーを起動
beforeAll(() => server.listen())

// 各テストの後にハンドラーをリセット(テスト間の干渉を防ぐ)
afterEach(() => server.resetHandlers())

// 全テストの後にサーバーを終了
afterAll(() => server.close())

React Hook Form のテスト

テストするフォームを作る

ログインフォームを例に使います。実務でよく見る構成です。

src/components/LoginForm.tsx

import { useForm } from 'react-hook-form'

type FormValues = {
  email: string
  password: string
}

type Props = {
  onSubmit: (data: FormValues) => void
}

export const LoginForm = ({ onSubmit }: Props) => {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormValues>()

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="email">メールアドレス</label>
        <input
          id="email"
          type="email"
          {...register('email', {
            required: 'メールアドレスを入力してください',
            pattern: {
              value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
              message: '正しいメールアドレスを入力してください',
            },
          })}
        />
        {errors.email && (
          <span role="alert">{errors.email.message}</span>
        )}
      </div>

      <div>
        <label htmlFor="password">パスワード</label>
        <input
          id="password"
          type="password"
          {...register('password', {
            required: 'パスワードを入力してください',
            minLength: {
              value: 8,
              message: 'パスワードは8文字以上で入力してください',
            },
          })}
        />
        {errors.password && (
          <span role="alert">{errors.password.message}</span>
        )}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'ログイン中...' : 'ログイン'}
      </button>
    </form>
  )
}

テストを書く

src/components/LoginForm.test.tsx

import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LoginForm } from './LoginForm'

describe('LoginForm', () => {

  const mockOnSubmit = vi.fn()

  // 各テストの前にモック関数をリセット
  beforeEach(() => {
    mockOnSubmit.mockClear()
  })

  describe('初期表示', () => {

    it('メールアドレスとパスワードの入力欄が表示される', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)

      expect(screen.getByLabelText('メールアドレス')).toBeInTheDocument()
      expect(screen.getByLabelText('パスワード')).toBeInTheDocument()
    })

    it('ログインボタンが表示される', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)

      expect(screen.getByRole('button', { name: 'ログイン' })).toBeInTheDocument()
    })

    it('初期状態ではエラーメッセージが表示されない', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)

      expect(screen.queryByRole('alert')).not.toBeInTheDocument()
    })

  })

  describe('バリデーション — 未入力', () => {

    it('何も入力せずsubmitすると、メールアドレスのエラーが表示される', async () => {
      const user = userEvent.setup()
      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.click(screen.getByRole('button', { name: 'ログイン' }))

      expect(await screen.findByText('メールアドレスを入力してください')).toBeInTheDocument()
    })

    it('何も入力せずsubmitすると、パスワードのエラーが表示される', async () => {
      const user = userEvent.setup()
      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.click(screen.getByRole('button', { name: 'ログイン' }))

      expect(await screen.findByText('パスワードを入力してください')).toBeInTheDocument()
    })

    it('バリデーションエラーがある場合、onSubmitは呼ばれない', async () => {
      const user = userEvent.setup()
      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.click(screen.getByRole('button', { name: 'ログイン' }))

      await waitFor(() => {
        expect(mockOnSubmit).not.toHaveBeenCalled()
      })
    })

  })

  describe('バリデーション — 不正な値', () => {

    it('不正なメールアドレスを入力するとエラーが表示される', async () => {
      const user = userEvent.setup()
      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.type(screen.getByLabelText('メールアドレス'), 'invalid-email')
      await user.click(screen.getByRole('button', { name: 'ログイン' }))

      expect(await screen.findByText('正しいメールアドレスを入力してください')).toBeInTheDocument()
    })

    it('8文字未満のパスワードを入力するとエラーが表示される', async () => {
      const user = userEvent.setup()
      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.type(screen.getByLabelText('パスワード'), 'short')
      await user.click(screen.getByRole('button', { name: 'ログイン' }))

      expect(await screen.findByText('パスワードは8文字以上で入力してください')).toBeInTheDocument()
    })

  })

  describe('正常系 — 正しい値を入力してsubmit', () => {

    it('正しい値を入力してsubmitすると、onSubmitが正しい値で呼ばれる', async () => {
      const user = userEvent.setup()
      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.type(screen.getByLabelText('メールアドレス'), 'test@example.com')
      await user.type(screen.getByLabelText('パスワード'), 'password123')
      await user.click(screen.getByRole('button', { name: 'ログイン' }))

      await waitFor(() => {
        expect(mockOnSubmit).toHaveBeenCalledWith({
          email: 'test@example.com',
          password: 'password123',
        })
      })
    })

    it('正しい値を入力してsubmitすると、エラーメッセージが表示されない', async () => {
      const user = userEvent.setup()
      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.type(screen.getByLabelText('メールアドレス'), 'test@example.com')
      await user.type(screen.getByLabelText('パスワード'), 'password123')
      await user.click(screen.getByRole('button', { name: 'ログイン' }))

      await waitFor(() => {
        expect(screen.queryByRole('alert')).not.toBeInTheDocument()
      })
    })

  })

})

RHFテストで詰まったポイント

ポイント1:findByText を使う

RHFのバリデーションは非同期で実行されます。getByText ではエラーメッセージが表示される前に検索してしまい、テストが失敗します。

// ❌ バリデーション結果が反映される前に検索してしまう
expect(screen.getByText('メールアドレスを入力してください')).toBeInTheDocument()

// ✅ DOMに現れるまで待ってから検索する
expect(await screen.findByText('メールアドレスを入力してください')).toBeInTheDocument()

findByText は要素が現れるまで最大1000ms待ちます。RHFのバリデーションテストでは基本的に findByText を使うと覚えておきましょう。

ポイント2:getByLabelText でフォーム要素を取得する

// ❌ placeholderで取得(ラベルがないとアクセシブルでない)
screen.getByPlaceholderText('メールアドレスを入力')

// ✅ labelのhtmlForとinputのidを紐付けてgetByLabelTextで取得
screen.getByLabelText('メールアドレス')

getByLabelText を使うためには、<label htmlFor="email"><input id="email"> が対応している必要があります。これはアクセシビリティ的にも正しい書き方です。テストを書くことで自然とアクセシブルなコードが書けるようになる、という好循環が生まれます。

ポイント3:beforeEach でモック関数をリセットする

beforeEach(() => {
  mockOnSubmit.mockClear()  // 呼び出し記録をリセット
})

mockClear() を忘れると、前のテストで mockOnSubmit が呼ばれた記録が残り、次のテストで「呼ばれていないはず」の確認が失敗します。


TanStack Query のテスト

テストするコンポーネントを作る

ユーザー一覧を取得・表示するコンポーネントです。

src/components/UserList.tsx

import { useQuery } from '@tanstack/react-query'

type User = {
  id: number
  name: string
  email: string
}

const fetchUsers = async (): Promise<User[]> => {
  const response = await fetch('/api/users')
  if (!response.ok) {
    throw new Error('ユーザーの取得に失敗しました')
  }
  return response.json()
}

export const UserList = () => {
  const { data, isLoading, isError, error } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  if (isLoading) {
    return <p>読み込み中...</p>
  }

  if (isError) {
    return <p role="alert">{(error as Error).message}</p>
  }

  return (
    <ul>
      {data?.map((user) => (
        <li key={user.id}>
          <span>{user.name}</span>
          <span>{user.email}</span>
        </li>
      ))}
    </ul>
  )
}

TanStack Queryのテスト用ラッパーを作る

TanStack Queryを使うコンポーネントは、QueryClientProvider でラップされていないとエラーになります。テストでも同じです。毎回ラッパーを書くのは面倒なので、共通のユーティリティを作っておきましょう。

src/test/utils.tsx

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, type RenderOptions } from '@testing-library/react'
import type { ReactElement } from 'react'

// テストごとに新しいQueryClientを作る(キャッシュが残らないようにするため)
const createTestQueryClient = () =>
  new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,       // テスト中はリトライしない
        gcTime: Infinity,   // テスト中はキャッシュを消さない
      },
    },
  })

// カスタムrenderラッパー
export const renderWithQueryClient = (
  ui: ReactElement,
  options?: Omit<RenderOptions, 'wrapper'>
) => {
  const queryClient = createTestQueryClient()

  const Wrapper = ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )

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

retry: false は特に重要です。デフォルトでは TanStack Query はリクエストが失敗すると3回リトライします。テスト中にリトライが走ると、テストが遅くなったり意図しない挙動になったりします。

テストを書く

src/components/UserList.test.tsx

import { screen, waitFor } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { server } from '../test/server'
import { renderWithQueryClient } from '../test/utils'
import { UserList } from './UserList'

// テスト用のダミーデータ
const mockUsers = [
  { id: 1, name: '田中太郎', email: 'tanaka@example.com' },
  { id: 2, name: '鈴木花子', email: 'suzuki@example.com' },
]

describe('UserList', () => {

  describe('ローディング状態', () => {

    it('データ取得中は「読み込み中...」を表示する', async () => {
      // レスポンスを遅延させてローディング状態を作る
      server.use(
        http.get('/api/users', async () => {
          await new Promise((resolve) => setTimeout(resolve, 100))
          return HttpResponse.json(mockUsers)
        })
      )

      renderWithQueryClient(<UserList />)

      expect(screen.getByText('読み込み中...')).toBeInTheDocument()

      // テストが終わるまでデータ取得を待つ(クリーンアップのため)
      await waitFor(() => {
        expect(screen.queryByText('読み込み中...')).not.toBeInTheDocument()
      })
    })

  })

  describe('エラー状態', () => {

    it('APIがエラーを返したとき、エラーメッセージを表示する', async () => {
      server.use(
        http.get('/api/users', () => {
          return HttpResponse.json(
            { message: 'Internal Server Error' },
            { status: 500 }
          )
        })
      )

      renderWithQueryClient(<UserList />)

      expect(await screen.findByRole('alert')).toHaveTextContent(
        'ユーザーの取得に失敗しました'
      )
    })

    it('ネットワークエラーのとき、エラーメッセージを表示する', async () => {
      server.use(
        http.get('/api/users', () => {
          return HttpResponse.error()
        })
      )

      renderWithQueryClient(<UserList />)

      expect(await screen.findByRole('alert')).toBeInTheDocument()
    })

  })

  describe('成功状態', () => {

    it('データ取得成功後、ユーザー一覧を表示する', async () => {
      server.use(
        http.get('/api/users', () => {
          return HttpResponse.json(mockUsers)
        })
      )

      renderWithQueryClient(<UserList />)

      expect(await screen.findByText('田中太郎')).toBeInTheDocument()
      expect(await screen.findByText('鈴木花子')).toBeInTheDocument()
    })

    it('データ取得成功後、メールアドレスも表示する', async () => {
      server.use(
        http.get('/api/users', () => {
          return HttpResponse.json(mockUsers)
        })
      )

      renderWithQueryClient(<UserList />)

      expect(await screen.findByText('tanaka@example.com')).toBeInTheDocument()
      expect(await screen.findByText('suzuki@example.com')).toBeInTheDocument()
    })

    it('ユーザーが0人のとき、リストが空になる', async () => {
      server.use(
        http.get('/api/users', () => {
          return HttpResponse.json([])
        })
      )

      renderWithQueryClient(<UserList />)

      await waitFor(() => {
        expect(screen.queryByText('読み込み中...')).not.toBeInTheDocument()
      })

      expect(screen.queryByRole('listitem')).not.toBeInTheDocument()
    })

  })

})

TanStack Queryテストで詰まったポイント

ポイント1:テストごとに QueryClient を新しく作る

// ❌ テスト間でQueryClientを共有する(キャッシュが残って干渉する)
const queryClient = new QueryClient()

// ✅ テストごとに新しく作る
const createTestQueryClient = () => new QueryClient({ ... })

TanStack Queryはキャッシュを持つのが特徴です。テスト間でキャッシュが残ると、「APIがエラーを返すテスト」の後に「成功するテスト」を実行したとき、キャッシュのせいでAPIを呼ばずに古いデータを返してしまいます。

ポイント2:server.use() でテストごとにハンドラーを上書きする

// setup.tsのafterEach(() => server.resetHandlers())と組み合わせることで
// 各テストで独立したAPIレスポンスを設定できる
server.use(
  http.get('/api/users', () => {
    return HttpResponse.json(mockUsers)
  })
)

server.resetHandlers()afterEach で呼んでいるので、テストが終わると server.use() で追加したハンドラーは自動的に消えます。テスト間の干渉がなくなります。

ポイント3:findByText で非同期データの表示を待つ

// ❌ データがまだ来ていないのにgetByTextで探す
expect(screen.getByText('田中太郎')).toBeInTheDocument()

// ✅ データが表示されるまで待つ
expect(await screen.findByText('田中太郎')).toBeInTheDocument()

APIのレスポンスは非同期です。getByText では要素がDOMに現れる前に探してしまいます。必ず findByTextwaitFor を使いましょう。


Part 3:フォームsubmit → API呼び出し → 結果表示の統合テスト

最後に、RHFとTanStack Queryを組み合わせたコンポーネントの統合テストを書きます。これが一番実務に近い内容です。

テストするコンポーネントを作る

ユーザー登録フォームです。submitするとAPIを叩いて、成功・失敗でUIが変わります。

src/components/RegisterForm.tsx

import { useMutation } from '@tanstack/react-query'
import { useForm } from 'react-hook-form'

type FormValues = {
  name: string
  email: string
}

type RegisterResponse = {
  id: number
  name: string
  email: string
}

const registerUser = async (data: FormValues): Promise<RegisterResponse> => {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  })

  if (!response.ok) {
    const error = await response.json()
    throw new Error(error.message || '登録に失敗しました')
  }

  return response.json()
}

export const RegisterForm = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>()

  const { mutate, isPending, isSuccess, isError, error, data } = useMutation({
    mutationFn: registerUser,
  })

  const onSubmit = (formData: FormValues) => {
    mutate(formData)
  }

  if (isSuccess) {
    return (
      <div role="status">
        <p>登録が完了しました!</p>
        <p>{data.name}さん、ようこそ!</p>
      </div>
    )
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="name">お名前</label>
        <input
          id="name"
          {...register('name', { required: 'お名前を入力してください' })}
        />
        {errors.name && <span role="alert">{errors.name.message}</span>}
      </div>

      <div>
        <label htmlFor="email">メールアドレス</label>
        <input
          id="email"
          type="email"
          {...register('email', { required: 'メールアドレスを入力してください' })}
        />
        {errors.email && <span role="alert">{errors.email.message}</span>}
      </div>

      {isError && (
        <p role="alert">{(error as Error).message}</p>
      )}

      <button type="submit" disabled={isPending}>
        {isPending ? '登録中...' : '登録する'}
      </button>
    </form>
  )
}

統合テストを書く

src/components/RegisterForm.test.tsx

import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { http, HttpResponse } from 'msw'
import { server } from '../test/server'
import { renderWithQueryClient } from '../test/utils'
import { RegisterForm } from './RegisterForm'

describe('RegisterForm', () => {

  describe('正常系 — 登録成功', () => {

    it('正しい値を入力してsubmitすると、成功メッセージが表示される', async () => {
      server.use(
        http.post('/api/users', () => {
          return HttpResponse.json({
            id: 1,
            name: '田中太郎',
            email: 'tanaka@example.com',
          })
        })
      )

      const user = userEvent.setup()
      renderWithQueryClient(<RegisterForm />)

      await user.type(screen.getByLabelText('お名前'), '田中太郎')
      await user.type(screen.getByLabelText('メールアドレス'), 'tanaka@example.com')
      await user.click(screen.getByRole('button', { name: '登録する' }))

      expect(await screen.findByText('登録が完了しました!')).toBeInTheDocument()
      expect(await screen.findByText('田中太郎さん、ようこそ!')).toBeInTheDocument()
    })

    it('登録成功後、フォームが非表示になる', async () => {
      server.use(
        http.post('/api/users', () => {
          return HttpResponse.json({
            id: 1,
            name: '田中太郎',
            email: 'tanaka@example.com',
          })
        })
      )

      const user = userEvent.setup()
      renderWithQueryClient(<RegisterForm />)

      await user.type(screen.getByLabelText('お名前'), '田中太郎')
      await user.type(screen.getByLabelText('メールアドレス'), 'tanaka@example.com')
      await user.click(screen.getByRole('button', { name: '登録する' }))

      await waitFor(() => {
        expect(screen.queryByRole('button', { name: '登録する' })).not.toBeInTheDocument()
      })
    })

  })

  describe('送信中の状態', () => {

    it('submitボタンをクリックすると「登録中...」に変わる', async () => {
      // レスポンスを遅延させてisPending状態を作る
      server.use(
        http.post('/api/users', async () => {
          await new Promise((resolve) => setTimeout(resolve, 200))
          return HttpResponse.json({ id: 1, name: '田中太郎', email: 'tanaka@example.com' })
        })
      )

      const user = userEvent.setup()
      renderWithQueryClient(<RegisterForm />)

      await user.type(screen.getByLabelText('お名前'), '田中太郎')
      await user.type(screen.getByLabelText('メールアドレス'), 'tanaka@example.com')
      await user.click(screen.getByRole('button', { name: '登録する' }))

      expect(screen.getByRole('button', { name: '登録中...' })).toBeDisabled()

      // 後処理
      await waitFor(() => {
        expect(screen.queryByRole('button', { name: '登録中...' })).not.toBeInTheDocument()
      })
    })

  })

  describe('異常系 — APIエラー', () => {

    it('APIが409を返したとき、エラーメッセージを表示する', async () => {
      server.use(
        http.post('/api/users', () => {
          return HttpResponse.json(
            { message: 'このメールアドレスはすでに登録されています' },
            { status: 409 }
          )
        })
      )

      const user = userEvent.setup()
      renderWithQueryClient(<RegisterForm />)

      await user.type(screen.getByLabelText('お名前'), '田中太郎')
      await user.type(screen.getByLabelText('メールアドレス'), 'tanaka@example.com')
      await user.click(screen.getByRole('button', { name: '登録する' }))

      expect(await screen.findByText('このメールアドレスはすでに登録されています')).toBeInTheDocument()
    })

    it('APIエラー後もフォームは表示されたまま', async () => {
      server.use(
        http.post('/api/users', () => {
          return HttpResponse.json(
            { message: 'サーバーエラー' },
            { status: 500 }
          )
        })
      )

      const user = userEvent.setup()
      renderWithQueryClient(<RegisterForm />)

      await user.type(screen.getByLabelText('お名前'), '田中太郎')
      await user.type(screen.getByLabelText('メールアドレス'), 'tanaka@example.com')
      await user.click(screen.getByRole('button', { name: '登録する' }))

      await waitFor(() => {
        expect(screen.getByRole('button', { name: '登録する' })).toBeInTheDocument()
      })
    })

  })

  describe('バリデーション', () => {

    it('未入力でsubmitするとAPIは呼ばれない', async () => {
      const requestSpy = vi.fn()

      server.use(
        http.post('/api/users', () => {
          requestSpy()
          return HttpResponse.json({ id: 1, name: '田中太郎', email: 'tanaka@example.com' })
        })
      )

      const user = userEvent.setup()
      renderWithQueryClient(<RegisterForm />)

      await user.click(screen.getByRole('button', { name: '登録する' }))

      await waitFor(() => {
        expect(requestSpy).not.toHaveBeenCalled()
      })
    })

  })

})

テスト設計で意識したこと

このシリーズを通じて学んだ、テストを書くときに意識すると良い考え方をまとめます。

「何をテストするか」を明確にする

テストは仕様書でもあります。it('...') の説明を読めば、そのコンポーネントが何をするべきかがわかるように書きましょう。

// ❌ 何をテストしているのかわかりにくい
it('正常に動作する', () => { ... })

// ✅ 「どんな状態のとき、何が起きるか」が明確
it('APIが409を返したとき、エラーメッセージを表示する', () => { ... })

ユーザーの行動に沿ってテストを書く

RTLの哲学通り、「ユーザーが実際にどう操作するか」を意識します。

1. フォームを開く
2. 名前を入力する
3. メールアドレスを入力する
4. 登録ボタンをクリックする
5. 成功メッセージが表示される

このフローがそのままテストコードになっています。テストコードを読めばユーザーストーリーがわかる、という状態が理想です。

正常系・異常系・境界値を網羅する

テストの種類
正常系 正しい値を入力してsubmit → 成功する
異常系(入力) 未入力・不正な値 → バリデーションエラー
異常系(API) 409・500エラー → エラーメッセージ
境界値 パスワード8文字でOK・7文字でNG
空データ ユーザー0人 → リストが空

まとめ

この記事で学んだことを振り返ります。

RHFフォームのテスト・MSWによるAPIモック・TanStack Queryのテスト・統合テストと、実務に直結する内容をカバーしました。

フレッシャーのときにここまで学べると、かなり強みになります。特に「フォームとAPIのテストが書ける」というのは、実務経験のある人でも苦手にしている人が多い領域です。

テストを書き続けることで気づいたことがあります。テストが書きにくいコードは、設計が良くないコードでもあるということです。テストを書こうとして「書きにくいな」と思ったら、それはコンポーネントを分割したり、責任を明確にするサインです。テストはコードの品質を鏡のように映します。


参考リンク


3部作最後まで読んでいただきありがとうございます。いいねやコメントいただけると励みになります!

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?