【TDD】テスト駆動開発を活用した安全なリファクタリング!
はじめに ~既存コードの修正って怖い~
こんにちは!ARMフロントエンド開発エンジニアの諸橋です!🦔
私はARMの自社サービスの開発者として、新たな機能を開発しています。
その際、既存のコードを修正することも多く、そのたびに「この修正で既存の処理に影響が出ないか心配だな...」という不安を感じることがあります。
そのため、普段コードを修正したときは、「既存の処理が問題なく動作していること(リグレッション試験)」と「新たな機能が期待通り動作していること」の2つを確認しています。
ただ、この方法だとコードを修正するたびに手間がかかり、開発効率が落ちてしまうことがあります。毎回画面を操作して確認するのは、正直かなり時間がかかりますよね…🌀
そんな課題を解決するため、テスト駆動開発について学び、チーム内での導入を進めています。
今回は、テスト駆動開発を活用した安全なリファクタの手順とメリットについてデモも含めてご紹介します!
テスト駆動開発とは テストを起点とした開発手法
テスト駆動開発(Test-Driven Development、略称TDD)とは、「テストを起点とした開発手法」です。
以下の3つのステップを繰り返すことで、開発を進めていきます。
- Red:テストを書いて失敗させる
- Green:テストを通すコードを書く
- Refactor(リファクタ):コードを整理する
開発現場では、Red → Green → Refactor という流れで表現されることが多いです。ちょっと信号機みたいですね🚥
それぞれのステップについて詳しく見ていきましょう!
1. Red:テストを書いて失敗させる
まずは、テストを書いて失敗させることから始めます。
このステップでは、まだ実装が存在しない状態で、あえてテストを失敗させるのがポイントです。
こうすることで、「次にどんな実装が必要なのか」を明確にできます。
2. Green:テストを通すコードを書く
次に、テストを通すための最小限のコードを書きます。
このステップでは、テストが成功することが最優先です。
そのため、実装の品質や効率はこの時点では考えません。
あくまでも、Red → Green に移行することがゴールになります。
3. Refactor(リファクタ):コードを整理する
最後に、コードを整理して品質を向上させるステップです✨
ここでは、コードの重複を除去したり、可読性を向上させたりするためのリファクタを行います。
ただし、過剰なリファクタは避け、本当に必要な部分に集中することが大切です。
また、リファクタするたびにテストを実行し、動作が変わっていないことを確認しながら進めるのがポイントです。
このRed → Green → Refactorのサイクルを回すことで、安全にリファクタを進めながら開発を行えるようになります!
テスト駆動開発が安全に、かつ正確な実装を実現
テスト駆動開発のメリットは、主に以下の3つがあります!
1. テストの自動化📝
テスト駆動開発では、「最初にテストを書く」ため、常に自動化されたテストがある状態で開発を進めることができます。
これにより、何度でも簡単にテストを実行できるようになり、バグの早期発見やリグレッションテストの実施がスムーズになります。
「後からテストを書こう」では、すべてのコードに対してテストがある保証はありません。しかし、TDDでは最初にテストを書くため、新しい機能を追加したときに「過去のテストをすべて実行して、影響がないか即座に確認できる」のが大きなメリットです!
2. リファクタの促進⚡**
テスト駆動開発では、「Red → Green → Refactor」のサイクルが開発フローに組み込まれています。
そのため、コードの品質や保守性を向上させるための「リファクタを安心して行える」ようになります。
例えば、関数の処理を整理したり、変数名を適切に変更したりする際も、「テストが通る限り、動作が変わっていないことを保証できる」ため、安心してリファクタができます。
3. 設計の改善🔨
テスト駆動開発では、「最初にテストを書くこと」が求められるため、設計を意識せざるを得ません。
「どのような入力があり、どのような出力が期待されるのか?」を事前に考えることで、無駄のないシンプルな設計が実現できます。
例えば、テストを書こうとしたときに、「この関数、引数が多すぎて扱いにくいな…」と気づいたら、その時点で適切な設計に修正できるのです。
このように、「テストを最初に書く」ことで、安全に、かつ正確に実装を進められることがわかりました!
では、すでにコードが書かれていて、その上で修正や機能追加を行う場合は、どう進めていけばいいのでしょうか?
次は、「既存のコードをテスト駆動開発で安全にリファクタする方法」について解説していきます!
既存コードの変更もテストとセットで進めよう!
「既存のコードに機能を追加する」場合も、テスト駆動開発の考え方を取り入れることで、安全に進めることができます。
細かく分解すると、以下のようなステップになります。
- 既存コードに対してテストを作成し、正常に動作することを確認する
- (必要であれば)既存コードをリファクタし、より簡潔で無駄のないコードに整理する
- Red:新機能のテストを作成し、テストを失敗させる
- Green:テストを通すコードを書く
- Refactor(リファクタ):コードを整理する
- 手順3〜5を繰り返す
最初に「既存コードに対するテストを書く」ステップが追加されているのがポイントですが、
その後は通常のテスト駆動開発の流れと同じですね!
【デモ】テスト駆動開発を使ったリファクタ・機能追加
ここからは、実際にコードに対して テストの作成、リファクタ、機能追加 までをやってみます!
以下の仕様に対して、新しい機能追加を依頼されたケースを想定して取り組みます。
【機能追加前】
以下の条件を満たすadvantage関数を実装せよ1から105まで順に数を数え上げていき
- 3の倍数なら「アドバン」
- 5の倍数なら「テッジ」
- 3と5の倍数なら「アドバンテッジ」
- いずれでもなければ、その数を返す
世の中には似たような問題として FizzBuzz問題 がありますね。
この仕様では、「1、2、アドバン、4、テッジ、アドバン、7…」というように出力されます。
ここからさらに、以下のような機能追加を行います。(太字部分が追加要件)
【機能追加後】
以下の条件を満たすadvantage関数を実装せよ1から105まで順に数を数え上げていき
- 3の倍数なら「アドバン」
- 5の倍数なら「テッジ」
- 7の倍数なら「リスクマネジメント」
- 3と5の倍数なら「アドバンテッジ」
- 3と7の倍数なら「アドバンリスクマネジメント」
- 5と7の倍数なら「テッジリスクマネジメント」
- 3と5と7の倍数なら「アドバンテッジリスクマネジメント」
- いずれでもなければ、その数を返す
ここで、7の倍数 に関する条件が追加されました。それに伴い、 3や5との掛け合わせ についても条件が増えていますね。
そして 3と5と7の倍数 の場合は、ついに 「アドバンテッジリスクマネジメント」 と社名そのままの文字列が出力されることになります!
社名の認知度を広げるためにも、ぜひ追加機能を実装したいところです!!
なお、今回は Jest という JavaScript のテストツールを使用します。
テストデモにフォーカスするため、Jestのセットアップ手順は割愛します。
使用ツール & ライブラリ
- エディタ: VSCode
- テストフレームワーク: Jest
- 拡張機能: Jest, Jest Runner
- 開発言語: JavaScript
手順1. 既存コードに対してテストを作成し、正常に動作することを確認する
まずは、既存のコードに対してテストを作成してみましょう。
機能追加前の仕様が実装された画面を確認します。
画面上では問題なく表示されているように見えますね。
advantage関数の実装の中身を確認すると、以下のようになっています。
// コードの内容
methods:{
advantage(param) {
if (param === 1) {
return "1";
}
if (param === 2) {
return "2";
}
if (param === 3) {
return "アドバン";
}
if (param === 4) {
return "4";
}
if (param === 5) {
return "テッジ";
}
if (param === 6) {
return "アドバン";
}
// 省略・・・
if (param === 104) {
return "104";
}
if (param === 105) {
return "アドバンテッジ";
}
}
}
……はい。3の倍数や5の倍数といった法則を無視して、単純に数値ごとに分岐させているコードになっています👿
かなり非効率なコードになっていますね。
まずは、このコードに対するテストを作成していきましょう!
// テスト内容
describe('アドバンテッジテスト', () => {
describe('3の倍数の確認', () => {
test('advantage関数が3の時、アドバンを返す', () => {
expect(script.methods.advantage(3)).toBe('アドバン');
});
test('advantage関数が6の時、アドバンを返す', () => {
expect(script.methods.advantage(6)).toBe('アドバン');
});
});
describe('5の倍数の確認', () => {
test('advantage関数が5の時、テッジを返す', () => {
expect(script.methods.advantage(5)).toBe('テッジ');
});
test('advantage関数が10の時、テッジを返す', () => {
expect(script.methods.advantage(10)).toBe('テッジ');
});
});
describe('15の倍数の確認', () => {
test('advantage関数が15の時、アドバンテッジを返す', () => {
expect(script.methods.advantage(15)).toBe('アドバンテッジ');
});
test('advantage関数が30の時、アドバンテッジを返す', () => {
expect(script.methods.advantage(30)).toBe('アドバンテッジ');
});
});
describe('それ以外の数の確認', () => {
test('advantage関数が1の時、1を返す', () => {
expect(script.methods.advantage(1)).toBe('1');
});
test('advantage関数が2の時、2を返す', () => {
expect(script.methods.advantage(2)).toBe('2');
});
});
});
さて、コードをざっと並べましたが、expect(script.methods.advantage(3)).toBe('アドバン');
の部分を簡単に説明します。
-
script.methods.advantage(3)
→ advantage関数に「3」を引数として渡す -
.toBe('アドバン')
→ その返却値が「アドバン」であることを検証する
このように、入力値と期待する出力を明示的にチェックしています。
そして、テストを実行した結果、すべてのテストがパス(成功)しました。
つまり、既存のコードは正しく動作していることが確認できました。(内容はともかく……👿)
また、テストは コードを修正するたびに自動実行 されるように設定しているので、問題があればすぐに検知できます。
これこそ、テスト駆動開発のメリットを実感できる瞬間ですね!
手順2. (必要であれば)既存コードに対して、より簡潔に、無駄のないコードにリファクタを行う
既存のコードが綺麗であればこの工程はスキップできますが、今回は明らかに改善できる点があるので、しっかり対応していきましょう。
「数を3や5で割ったときの余りを使った条件式」 に変更し、スッキリとしたコードにリファクタしました!
advantage(param) {
if (param % 15 === 0) {
return "アドバンテッジ";
}
if (param % 3 === 0) {
return "アドバン";
}
if (param % 5 === 0) {
return "テッジ";
}
return param.toString();
},
はい、めちゃくちゃシンプルになりましたね✨
これで、if
文をひたすら羅列していた 「劣悪なコード👿」 から、 「スマートなコード😇」 に進化しました!
修正後は、必ずテストを実行しましょう!
テストが全てパスすれば、「既存の処理が正常に動作する」+「よりシンプルなコード設計が実現できた」 という最高の結果になります!
では、この勢いのまま、新たな機能の試験を作っていきましょう!
手順3. Red:新機能に対するテストを作成し、テストを失敗させる
次に、新機能である 「7の倍数に対する制御」 を追加していきます!
まずは、「7の倍数なら 'リスクマネジメント' を返す」 という仕様のテストを作成しました。
describe('7の倍数の確認', () => {
test('advantage関数が7の時、リスクマネジメントを返す', () => {
expect(script.methods.advantage(7)).toBe('リスクマネジメント');
});
test('advantage関数が14の時、リスクマネジメントを返す', () => {
expect(script.methods.advantage(14)).toBe('リスクマネジメント');
});
});
テストを書いたら、さっそく実行してみましょう!
・・・が、当然エラーになります。
なぜなら、現時点ではadvantage関数に 7の倍数に関する処理がない からです。
ですが、これは テスト駆動開発の基本的な流れ なので問題ありません!
「まずはテストを書き、失敗させる(Red)」 → 「次にコードを書いて成功させる(Green)」 という流れで進めていきます。
では、このエラーを解消するためのコードを書いていきましょう!
手順4. Green:テストを通すコードを書く
先ほど作成したテストを通すために、7の倍数に関する処理を追加しました!
advantage(param) {
if (param % 15 === 0) {
return "アドバンテッジ";
}
if (param % 3 === 0) {
return "アドバン";
}
if (param % 5 === 0) {
return "テッジ";
}
// 7の倍数に関する処理を追加
if (param % 7 === 0) {
return "リスクマネジメント";
}
return param.toString();
},
コードを修正したら、テストを再度実行しましょう!
結果:すべてのテストがパス(成功)しました!🎉
これで、「7の倍数なら 'リスクマネジメント' を返す」 という仕様がしっかり実装され、期待通りに動作することが確認できました!
次は、リファクタを行い、より効率的なコードにしていきます!
手順5. Refactor(リファクタ):コードを整理する
作成したコードに対して、リファクタの余地がないか確認します。
今回は特に修正すべき点はなさそうですね!
このように、リファクタしない判断も重要 です。
🛠 普段チェックするリファクタの観点
- 条件分岐が整理されているか(不要な処理がないか)
- 同じ処理を繰り返していないか(DRY原則)
- 可読性を向上できるか(理解しやすいコードか)
- 意図しない動作を防げるか(バグが入りにくい構造か)
このような視点を持つことで、今後の開発でもより良いコードを書けるようになります!
それでは、新しい機能の開発に進みましょう!
手順6. 手順3~手順5を繰り返す
「red-green-refactor」のサイクルを意識しながら、「21の倍数、35の倍数、105の倍数」への対応を進めていきます。
自動テストを活用することで、安全に正確に実装を進められます。
このプロセスを繰り返すことで、バグの混入を防ぎつつ、拡張しやすいコードを作ることができますね。
最終的に・・・社名は出せたのか・・・?
追加機能の実装を終え、無事に105の結果である「アドバンテッジリスクマネジメント」の出力まで実装を行うことができました!
途中既存の処理が誤った結果になってしまう不具合が起きましたが、自動テストを行っていたことによりすぐに検知できました!
リファクタについても、テストを最初に書くことで「安全に、かつ正確に」進めることができることを伝えられたかと思います!
終わりに〜実際にプロジェクトへの導入に向けて〜
勉強会にてデモも含め本記事の内容を発表した中で以下のような声をいただきました。!
いただいたフィードバック
「保存した瞬間にフィードバックを得れるスピード感、安定感が良いなと思いました!」
「テストケースの洗い出し、どこまでチェックするのかについては好みが出そうですね」
「実際のサービスに導入する際、各関数に対してどこまでテストするのかはチーム内で方針を決める必要がある」
テスト駆動開発についてポジティブな意見をもらいつつ、「そのテストでどこまでを確認するのか」については引き続きチームで決めていく必要があることがわかりました。
まずはプロジェクトに導入し、処理が明確な関数から試験を入れていきたいと考えています。
テスト駆動開発について皆さんからの反応が良ければ、プロジェクト導入についても取り組みを記事にまとめていきたいですね。
少し長くなりましたが、最後まで読んでいただきありがとうございました!
株式会社アドバンテッジリスクマネジメント(ARM)では、一緒に働く仲間を募集しています!
興味を持っていただけた方は、ぜひ以下のリンクから詳細をご覧ください
Discussion