AIがコード書いてくれる時代こそ、駆け出しエンジニアはTDDと要件定義から勉強しろ
はじめに
AIにコードを書かせると、一瞬で動くものが出てくる。
「すごい、もうエンジニアいらないじゃん」
違います。
動くけど要件を満たしていない。
動くけどテストできない。
動くけど、なぜこう書いたのか誰も説明できない。
AI駆動開発、バイブコーディングというので、すごいコード書いてくれるじゃんと中身を見ずに開発してレビューでコテンパンにされた自分が経験したことからこれが必要だと思ったので書きます。
本当によくあるパターン
「AIに丸投げして全部壊れました」みたいな派手な失敗は、実はあんまりない。
本当によくあるのは、こっち。
「なんとなく動いてるからそのまま進めちゃう」
これが一番怖い。
動いてるから問題に気づけない。
気づいたときには、コードが膨れ上がっていて手が付けられない。
なぜそうなるのか
要件が曖昧だから。
「社内の問い合わせに自動回答するやつ作って」
これ、要件じゃない。願望です。
- どんな問い合わせに答えるのか?
- 答えられない質問はどうするのか?
- 精度はどこまで求めるのか?
- そもそもこの機能、本当に必要なのか?
この問いに答えられないまま実装を始めると、
AIが書いたコードが正しいかどうか、自分で判断できない。
要件定義は「何を作るか」だけの話じゃない
ここが一番伝えたいこと。
要件定義をやると、
「何を実装するか」が明確になる。
それは当たり前。
でもそれだけじゃない。
実装者であると同時に、もう一つ別の目線を持てるようになる。
- この機能、ユーザーが本当に求めているのか?
- 今やるべきか、後でもいいのか?
- 工数に見合う価値があるのか?
これはビジネス的な判断でもある。
駆け出しエンジニアは、言われたものを作ることが仕事だと思いがち。
でも要件定義をやると、「作らない判断」もできるようになる。
それが開発者としての視座が上がるということ。
要件を書いてみる
じゃあ実際に書いてみる。
## 要件
- 社内のよくある質問に対して、過去のドキュメントから回答を生成する
- 回答には参照元のドキュメントリンクを含める
- 回答の信頼度が低い場合は「担当者に聞いてください」と返す
- レスポンスは3秒以内
## やらないこと
- 社外向けの問い合わせ対応(スコープ外)
- 回答の自動学習(v1では手動でドキュメントを追加)
たったこれだけ。
でもこれを書くか書かないかで、全然違う。
「やらないこと」を明記するのが特に大事。
AIは何も言わなければ何でも作ろうとする。
TDDとは何か
要件が決まったら、次はTDD。
TDD(テスト駆動開発)は、こういうサイクルで開発を回す。
🔴 Red:まずテストを書く。当然落ちる。
実装がまだないから、テストは失敗する。
これが正常。
test('信頼度が低ければ、担当者に聞いてくださいと返す', () => {
const generator = new AnswerGenerator(mockLLM)
const result = await generator.generate('来年の給与は?', emptyDocs)
expect(result.answer).toBe('担当者に聞いてください')
expect(result.confidence).toBeLessThan(0.5)
})
この時点でAnswerGeneratorクラスは存在しない。
テストは赤。
🟢 Green:テストが通る最小限の実装を書く。
ここでAIに書かせる。
「このテストが通るようにAnswerGeneratorを実装して」
大事なのは「最小限」。
テストが通ればいい。きれいじゃなくていい。
class AnswerGenerator {
constructor(private llm: LLMClient) {}
async generate(question: string, docs: Document[]) {
if (docs.length === 0) {
return { answer: '担当者に聞いてください', confidence: 0, sources: [] }
}
const response = await this.llm.ask(question, docs)
return {
answer: response.text,
confidence: response.confidence,
sources: docs.map(d => ({ title: d.title, url: d.url }))
}
}
}
テストが緑になった。
🔵 Refactor:テストが通ったまま、コードを整理する。
テストが通っている状態を維持しながら、
構造を整理する。変数名を直す。重複を消す。
テストがあるから、リファクタリングしても壊れたらすぐ分かる。
安心して直せる。
このサイクルを小さく回す
1つの要件に対して、1つのテスト。
1つのテストに対して、1つの小さな実装。
AIに「全部作って」と投げるのではなく、
1サイクルごとに小さく任せる。
これがAI時代のTDD。
テストが書けない?それ設計が悪い
ここでもう1つ大事な話。
TDDのサイクルを回そうとすると、
テストが書けないコードにぶつかる。
たとえば、最初にAIが一発で生成したこのコード。
// AIに「チャットボット作って」で出てきたコード
async function handleQuestion(question: string) {
const docs = await fetch('/api/search', {
method: 'POST',
body: JSON.stringify({ query: question })
}).then(r => r.json())
const answer = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { 'x-api-key': process.env.API_KEY },
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
messages: [{ role: 'user', content: `${docs}を参考に回答して: ${question}` }]
})
}).then(r => r.json())
await db.insert(logs).values({
question, answer: answer.content[0].text, createdAt: new Date()
})
return answer.content[0].text
}
この関数、テスト書けますか?
- fetchが直接呼ばれてる
- DBに直接書き込んでる
- 全部が1つの関数に入ってる
- 信頼度の判定がそもそもない
テストが書けないコード = 設計が悪いコード。
TDDのサイクルを回そうとすると、自然とこうなる:
- 1クラス1責務になる(回答生成と信頼度判定は別クラス)
- 依存が外から注入される(LLMクライアントをコンストラクタで渡す)
- 副作用が隔離される(DB保存はRepositoryに切り出す)
これは「きれいなコードを書きましょう」という道徳の話じゃない。
テストを書こうとしたら、勝手にこうなる。
TDDができる = 設計ができている
テストを細かくチューニングできるということは、
要件を細かく分解できているということ。
要件を細かく分解できているということは、
責務を正しく分離できているということ。
全部つながっている。
駆け出しにこそ必要な理由
駆け出しエンジニアは:
- 要件を言語化する経験が少ない
- 設計の良し悪しの判断基準がまだない
- AIの出力が正しいかどうか分からない
- 言われたものを作ることが仕事だと思っている
だからこそ、
要件定義をやることで、
「本当に必要なのか」を考える目線が手に入る。
テストを先に書くことで、
AIの出力の「正しさ」を自分で判定できるようになる。
TDDのサイクルを回すことで、
テストが書ける設計=良い設計を自然に身につけられる。
まとめ
AIがコードを書いてくれる時代に、
駆け出しエンジニアが身につけるべきは、
- コードを速く書く力、ではない
- AIツールを使いこなす力、でもない
要件定義。TDD。設計。
この3つは別々のスキルじゃない。
Red → Green → Refactor のサイクルの中で、全部つながっている。
AIに任せるからこそ、
自分は上流に立て。
AI時代でも、ビジネス目線で働けるエンジニアはこれからも重宝される。
Discussion
AIが一発で出したhandleQuestion関数の例、まさに「テストが書けないコード=設計が悪いコード」を体現していて分かりやすかったです。信頼度判定を別クラスに切り出す、DBをRepositoryに逃がすというのも、TDDを回そうとした結果として自然に出てくる設計だという流れが説得力あります。