頑張らないTDD with React ― TypeScript×Jest×Testing LibraryでカードUIを最短実装
はじめに:なぜ「頑張らないTDD」なのか
恥の多い生涯を送って来ました。 自分には、テストコードというものが、見当つかないのです。
何を隠そう、エンジニアとして苦節6年半働いていた私はテストコードというものを書いたことがありません。 このままでは、「エンジニア失格」という本を執筆することになってしまいそうですが、テストコードを書くことで品質を担保しつつ開発を進めたい欲はあったので、できるだけ楽に開発する手法を検討することにしました。
前提:TDDとは
TDDは Test-Driven Development (=テスト駆動開発) の略称であり、テストを基に実装を進める開発手法です。 TDDでは下記の3ステップのサイクルを繰り返すことで開発を行います。
- Red
- 実装したい機能に対するテストコードを実装する。この時点ではテストが失敗するが、その前提で作って問題ない。
- Green
- テストが通るように最低限のコードを実装する。
- Refactor
- コードの品質向上のために必要に応じてリファクタを行う。
TDDでは様々なメリットがあります。 その一例としては、やはり品質の向上があると思います。 DevOps的な観点で考えるとCIに含めることで、素早く簡単にバグを見つけることが可能となり、自然に対応も素早くなります。
生成AIを使って楽にできないか?
ということで本題です。 テストコードを書くことは個人的にハードルが高い上、昔に同期が言っていた「テストコード書くのつまらん。機能実装の方が楽しい」という言葉が記憶に残っているのでテストコードはできるだけ書きたくありません。 ただ、生成AIの進化が凄まじい昨今では、コードを書く労力は少なく済むため、コードの記述に問題がないかのレビューさえできれば迅速に高品質のコードができるのではないかと考えました。 そのため、本来のTDDに沿って開発サイクルを考えると下記のようになるかと思います。
- Red
- 実装したい機能をプロンプトにして生成AIに投げる。生成AIが出力したコードに問題がないか確認し、問題となる箇所は修正する。
- Green
- 生成されたテストコードを基に実装コードを生成するプロンプトを生成AIに投げる。生成AIが出力したコードに問題がないか確認し、問題となる箇所は修正する。
- 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
- スートによって色を変えてください
- スペードの場合は黒
- ハートの場合は赤
- クラブの場合は緑
- ダイヤの場合は青
- 不正な文字の場合は何も表示しない
What is BEMA!?
Be Engineer, More Agile
Advent Calendar!
Advent Calendar 2024