BEMAロゴ

エンジニアの
成長を支援する
技術メディア

頑張らないTDD with React ― TypeScript×Jest×Testing LibraryでカードUIを最短実装

はじめに:なぜ「頑張らないTDD」なのか

恥の多い生涯を送って来ました。 自分には、テストコードというものが、見当つかないのです。

何を隠そう、エンジニアとして苦節6年半働いていた私はテストコードというものを書いたことがありません。 このままでは、「エンジニア失格」という本を執筆することになってしまいそうですが、テストコードを書くことで品質を担保しつつ開発を進めたい欲はあったので、できるだけ楽に開発する手法を検討することにしました。

前提:TDDとは

TDDは Test-Driven Development (=テスト駆動開発) の略称であり、テストを基に実装を進める開発手法です。 TDDでは下記の3ステップのサイクルを繰り返すことで開発を行います。

  1. Red
    • 実装したい機能に対するテストコードを実装する。この時点ではテストが失敗するが、その前提で作って問題ない。
  2. Green
    • テストが通るように最低限のコードを実装する。
  3. Refactor
    • コードの品質向上のために必要に応じてリファクタを行う。

TDDでは様々なメリットがあります。 その一例としては、やはり品質の向上があると思います。 DevOps的な観点で考えるとCIに含めることで、素早く簡単にバグを見つけることが可能となり、自然に対応も素早くなります。

生成AIを使って楽にできないか?

ということで本題です。 テストコードを書くことは個人的にハードルが高い上、昔に同期が言っていた「テストコード書くのつまらん。機能実装の方が楽しい」という言葉が記憶に残っているのでテストコードはできるだけ書きたくありません。 ただ、生成AIの進化が凄まじい昨今では、コードを書く労力は少なく済むため、コードの記述に問題がないかのレビューさえできれば迅速に高品質のコードができるのではないかと考えました。 そのため、本来のTDDに沿って開発サイクルを考えると下記のようになるかと思います。

  1. Red
    • 実装したい機能をプロンプトにして生成AIに投げる。生成AIが出力したコードに問題がないか確認し、問題となる箇所は修正する。
  2. Green
    • 生成されたテストコードを基に実装コードを生成するプロンプトを生成AIに投げる。生成AIが出力したコードに問題がないか確認し、問題となる箇所は修正する。
  3. Refactor
    • コードの品質向上のために必要に応じてリファクタを行う。

このパターンの実践例として、Reactでトランプのコンポーネントを開発してみようと思います。

環境

  • Next.js
    • 15.4.4
  • React.js
    • 19.1.0
  • jest
    • 30.0.5

実装

3ステップに分けて実装の流れを記述していこうと思います。

Red

まずはテストを実行するための空のコンポーネントを作成します。
本件はコードが簡潔なため、生成AIは用いず人力で実装しました。

components/CardDisplay.tsx

const CardDisplay = () => {

  return (

    <>

    </>

  )

}


export default CardDisplay;

テストコードの作成は工数がかかってしまうので生成AIに仕様をプロンプト(※)に含めて投げるといい感じに作成してくれます。 実際に投げてみると、 Card という型を別ファイルで生成していたので、そちらも併せて作成します。

※ こちらのプロンプトが見つからなかったので記憶ベースのものとなりますが、記事の最後に記載しておきます

domain/types/Card.ts

export type Card = ${'S' | 'H' | 'D' | 'C'}${'A' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'T' | 'J' | 'Q' | 'K'} | ''
components/test/CardDisplay.test.tsx

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

import '@testing-library/jest-dom'

import CardDisplay from './CardDisplay'

import { Card } from '@/domain/types/Card'

describe('CardDisplay', () => {

  it('スペードのAを正しく表示する', () => {

    const card: Card = 'SA'

    render(<CardDisplay card={card} />)

    expect(screen.getByText('♠')).toBeVisible()

    expect(screen.getByText('♠').parentElement).toHaveClass('text-black')

    expect(screen.getByText('A')).toBeVisible()

  })

  it('ハートのKを正しく表示する', () => {

    const card: Card = 'HK'

    render(<CardDisplay card={card} />)

    expect(screen.getByText('♥')).toBeVisible()

    expect(screen.getByText('♥').parentElement).toHaveClass('text-rose-600')

    expect(screen.getByText('K')).toBeVisible()

  })

  it('ダイヤの10を正しく表示する', () => {

    const card: Card = 'DT'

    render(<CardDisplay card={card} />)

    expect(screen.getByText('♦')).toBeVisible()

    expect(screen.getByText('♦').parentElement).toHaveClass('text-indigo-600')

    expect(screen.getByText('T')).toBeVisible()

  })

  it('クラブの5を正しく表示する', () => {

    const card: Card = 'C5'

    render(<CardDisplay card={card} />)

    expect(screen.getByText('♣')).toBeVisible()

    expect(screen.getByText('♣').parentElement).toHaveClass('text-green-600')

    expect(screen.getByText('5')).toBeVisible()

  })

  it('不正なスートの場合は何も表示しない', () => {

    // @ts-expect-error テスト用に不正な値を渡す

    render(<CardDisplay card={'X9'} />)

    expect(screen.queryByText('♠')).toBeNull()

    expect(screen.queryByText('♥')).toBeNull()

    expect(screen.queryByText('♦')).toBeNull()

    expect(screen.queryByText('♣')).toBeNull()

    expect(screen.queryByText('9')).toBeNull()

  })

})

この時点ではテストが失敗していても問題ありません。 ここからテストが成功するように実装していきます。

Green

テストコードをプロンプトに含めて、テストが達成するようにコードを生成するようなプロンプトを組み、生成AIに投げます。 GitHub Copilotをお使いの方はエディタ上でいつも通りTabキー連打すれば生成できます。

components/CardDisplay.tsx

import { Card } from '@/domain/types/Card';


const suitSymbols = {

  S: '♠',

  H: '♥',

  D: '♦',

  C: '♣',

};

export interface CardDisplayPropTypes {

    card: Card

}

function suitToTextColor(suit: keyof typeof suitSymbols): string {

  switch (suit) {

    case 'S':

      return 'text-black';

    case 'C':

      return 'text-green-600';

    case 'H':

      return 'text-rose-600';

    case 'D':

      return 'text-indigo-600';

    default:

      return '';

  }

}

const CardDisplay = (props: CardDisplayPropTypes) => {

  const { card } = props

  if (!card || /^[SHDC][A23456789TJQK]$/.test(card) === false) {

    return (

      <div className="border rounded-md p-4 w-20 h-28 flex flex-col justify-center items-center shadow-sm">

      </div>

    )

  }

  const suit = props.card as keyof typeof suitSymbols

  const rank = props.card.substring(1)

  return (

    <div className={`border rounded-md p-4 w-20 h-28 flex flex-col justify-center items-center shadow-sm ${suitToTextColor(suit)}`}>

      <span className="text-2xl">{suitSymbols[suit]}</span>

      <span className="text-xl font-bold">{rank}</span>

    </div>

  )

}

export default CardDisplay;

Refactor

必要に応じてリファクタを行います。 今回は実装範囲が少ないので省略します。

感想

生成AIを利用して開発してると「よく分からないが通る」コードが生成されることがあると思います。
ただ、そこにTDDを組み込むと、「テストが通ること」を確認するので動作を担保することができ、「よく分からないが通る」ことを減らすことができます。 生成AIを味方につけることで、TDDはもはや「コスト」ではなく、品質とスピードを両立させるための強力な「武器」になりそうです。

補足

記事途中にあったテストコードを生成するプロンプトですが、下記に記憶ベースのものを記載しておきます。このプロンプトでテストコードを生成できることは確認しましたが、上記と全く同一のコードが生成されることを保証するものではないため、ご了承ください。

## 概要

トランプのカードを表示するためのコンポーネント CardDisplay.ts をテストするためのコードを生成してください

## フレームワーク

- Next.js
- Tailwindcss

## 仕様

- propsは2文字としてください
  - 1文字目はスートとして扱ってください

    - Sの場合は♠︎
    - Hの場合は❤︎
    - Cの場合は♣︎
    - Dの場合は♦︎
  - 2文字目は数字(ランク)として扱ってください
    - Tの場合は10
- スートによって色を変えてください
  - スペードの場合は黒
  - ハートの場合は赤
  - クラブの場合は緑
  - ダイヤの場合は青
- 不正な文字の場合は何も表示しない

この記事が役に立ったと思ったら、
ぜひ「いいね」とシェアをお願いします!

リンクをコピーXでシェアするfacebookでシェアする

この記事を書いた人

三上 柊悟
三上 柊悟
2024年6月にメンバーズに中途入社。インフラ構築・開発・運用の実務経験、現在に至るまで複数のクライアントワークを行なっている。
詳しく見る
ページトップへ戻る