この記事に書かれていること
- Java初級者が
- テスト駆動開発で
- うるう年判定機能を開発してみた
書籍:テスト駆動開発 に則った手順で開発し、その流れを再現しました。
参考文献:テスト駆動開発
テストコード&プロダクションコードを記載しました。
「こうした方がいいよ」といった助言歓迎です。
開発環境
- Eclipse: 2023-09 (4.29.0)
- OS: Windows 11
- Java: 17.0.9
- JUnit5
書かれていないこと
- テスト駆動開発の詳細な説明
- 環境設定手順
書籍読者もしくは、既にテスト駆動開発をご存じの方向けです。
あくまで復習として書いた記事です。
テストコード完成形
テストコード
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
// 西暦年が4で割り切れる年は(原則として)閏年。
// ただし、西暦年が100で割り切れる年は(原則として)平年。
// ただし、西暦年が400で割り切れる年は必ず閏年。
class LeapYearTest {
@Test
@DisplayName("isLeapYear()のテスト:入力値がうるう年(4で割り切れる数字)ならtrueを返す")
void testOfLeapYear() {
LeapYear LY = new LeapYear();
int leapyear = 2004;
int commonyear = 2005;
assertTrue(LY.isLeapYear(leapyear));
assertFalse(LY.isLeapYear(commonyear));
}
@Test
@DisplayName("isLeapYear()のテスト:入力値が100で割り切れる(平年)ならfalseを返す")
void testOf100Year() {
LeapYear LY = new LeapYear();
int hundredyear = 100;
assertFalse(LY.isLeapYear(hundredyear));
}
@Test
@DisplayName("isLeapYear()のテスト:入力値が400で割り切れる(うるう年)ならtrueを返す")
void testOf400Year() {
LeapYear LY = new LeapYear();
int four_hundredyear = 400;
assertTrue(LY.isLeapYear(four_hundredyear));
}
}
プロダクトコード完成形
プロダクトコード
class LeapYear {
private final boolean leapyear = true;
private final boolean commonyear = false;
boolean isLeapYear(int year) {
if (year % 400 == 0) return leapyear;
if (year % 100 == 0) return commonyear;
if (year % 4 == 0) return leapyear;
return commonyear;
}
}
①やることリスト
テスト駆動開発をするにあたって、ちょうどいい課題はないかと探していました。
調べてみると、先人がうってつけの記事を紹介してくれていました。
↓
Cyber-Dojo で Fizz Buzz の次におすすめするエクササイズ5選
その中でも、「これなら実装できそう」と思えたのが「うるう年判定」。
どういう機能を満たしていればいいのか?
調べたところ、
- 西暦年が4で割り切れる年は(原則として)うるう年
- ただし、西暦年が100で割り切れる年は(原則として)平年
- ただし、西暦年が400で割り切れる年は必ずうるう年
という条件。
これなら if 文を使えば実装できると判断。
まずは
- テストクラスの作成
- 実装すべき機能のメモ
にとりかかりました。
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
// 西暦年が4で割り切れる年は(原則として)閏年。
// ただし、西暦年が100で割り切れる年は(原則として)平年。
// ただし、西暦年が400で割り切れる年は必ず閏年。
class LeapYearsTest {
}
②失敗するテストコード作成
「どういうテストをクリアしてほしいか?」
から出発して、テストを書いてみました。
今回のお題なら、まずこの機能を満たしてほしい。
- 西暦年が4で割り切れる年は(原則として)うるう年
想定したことは以下。
- LeapYears というクラスがあってほしい
- うるう年か判定する関数があってほしい
- その関数に int 型の変数 year を渡したら、 true/false を返してほしい
これらを確かめてくれるテストを書いてみました。
class LeapYearsTest {
@Test
@DisplayName("calculate()のテスト:入力値がうるう年(4で割り切れる数字)ならtrueを返す")
void testCalculateOfLeapYear() {
LeapYears LY = new LeapYears();
int leapyear = 2004;
assertTrue(LY.calculate(leapyear));
}
}
判定する関数は calculate という名前が適切なのだろうか?
とりあえず仮置きしました。
テストを実行すると、レッドバーが出ました。これでよし。
③テストをクリアする実装に移る
テストは合格しなければいけないので、ここで初めて実装します。
該当のクラスを作り、関数作成。
ただ、仮実装段階なのでまだ作り込みません。
true を返すだけ。
class LeapYears {
boolean calculate(int year) {
return true;
}
}
テストを実行してみたところ、グリーンバー。やった。
④リファクタリング
遠回りな気もしますが、テスト駆動開発の手順通りです。
- テスト作成
- (仮実装 &) 開発
- リファクタリング
ただでさえ私はプログラミングに慣れていないので、
横着せずにこの方針に則りました。
現状だと、どんな年を渡してもうるう年判定されてしまいます。
そのため、
- テストをクリアしつつ
- 適切な処理ができるように
修正します。
class LeapYears {
boolean calculate(int year) {
if (year % 4 == 0) return true;
return false;
}
}
テストを実行してみたところ、変わらずグリーンバー。
と、ここで気づきました。
平年のときは平年と判定してもらわないと困る!
調べるテストを追加。
@Test
@DisplayName("calculate()のテスト:入力値が平年(4で割り切れない数字)ならfalseを返す")
void testCalculateOfPlainYear() {
LeapYears LY = new LeapYears();
int commonyear = 2005;
assertFalse(LY.calculate(commonyear));
}
再度テスト実行。グリーンバーでした。
これで機能1つ目は達成。
- 西暦年が4で割り切れる年は(原則として)うるう年
- ただし、西暦年が100で割り切れる年は(原則として)平年
- ただし、西暦年が400で割り切れる年は必ずうるう年
⑤テストと開発を続ける
次の機能の実装に移ります。
テストが先だと言い聞かせつつ。
- ただし、西暦年が100で割り切れる年は(原則として)平年
これを満たすかどうかを調べられる、新しいテストコードを作ります。
一つ目のテストを再利用して、
与える year の値を変えるだけで済みそう。
@Test
@DisplayName("calculate()のテスト:入力値が100で割り切れる(平年)ならfalseを返す")
void testCalculateOf100Year() {
LeapYears LY = new LeapYears();
int hundredyear = 100;
assertFalse(LY.calculate(hundredyear));
}
テストを実行してみると、レッドバー。
今のコードだと、100も4で割り切れてうるう年と判定されるため。
修正します。
class LeapYears {
boolean calculate(int year) {
if (year % 100 == 0) return false;
if (year % 4 == 0) return true;
return false;
}
}
if-else 文でもいい気がしましたが、 if 文は短く収めたいです。
(リファクタリングでいうガード節の練習にもなるし)
ここは if 文をもう一つ用意しました。
テストを実行すると、グリーンバーに戻りました。
これで機能がもう一つできました。
- 西暦年が4で割り切れる年は(原則として)閏年
- ただし、西暦年が100で割り切れる年は(原則として)平年
- ただし、西暦年が400で割り切れる年は必ず閏年
それでは最後の機能。
- ただし、西暦年が400で割り切れる年は必ず閏年
100で割り切れても、400で割り切れる年はうるう年とのこと。
それなら、また if 文を追加するだけで大丈夫そう……おっと危ない。テストが先。
@Test
@DisplayName("calculate()のテスト:入力値が400で割り切れる(うるう年)ならtrueを返す")
void testCalculateOf400Year() {
LeapYears LY = new LeapYears();
int four_hundredyear = 400;
assertTrue(LY.calculate(four_hundredyear));
}
テストを実行すると、またレッドバー。想定通り。
コードを修正。
class LeapYears {
boolean calculate(int year) {
if (year % 400 == 0) return true;
if (year % 100 == 0) return false;
if (year % 4 == 0) return true;
return false;
}
}
テストを実行すると、グリーンバー。
これでよし。
⑥またリファクタリング
それでは再度リファクタリング。
と言っても、どこを直せばいいんだろう?
class LeapYears {
boolean calculate(int year) {
if (year % 400 == 0) return true;
if (year % 100 == 0) return false;
if (year % 4 == 0) return true;
return false;
}
}
パッと見で気になる点。
- if 文が連続しているのは、鬱陶しく見える
- if 文の条件がちょっと複雑かも。空目してしまうかもしれない
でも、これを解消するにはどうしたらいいんだろう?
リファクタリングで言うポリモーフィックを使ってみるとか?ファクトリメソッドを使ってみる?
でもそんな複雑な処理をしているわけじゃないし。
手段はあったとしても、自分が知らないかも。
他の誰かがこのコードを読んだ時を想像してみます。
その人がすぐにこのコードの振る舞いを分かるためには、どうすればいいか?
「この関数はうるう年判定をする」 という主張が伝わる内容にしたい。
と、ここで思いついた。
if 文もですが、この戻り値 true/false の部分がちょっと不親切かもしれない。
これを、いわゆる説明変数にしてしまえばどうだろう。
こんな感じに。
class LeapYears {
private final boolean leapyear = true;
private final boolean commonyear = false;
boolean calculate(int year) {
if (year % 400 == 0) return leapyear;
if (year % 100 == 0) return commonyear;
if (year % 4 == 0) return leapyear;
return commonyear;
}
}
こうすれば、いくらか読みやすくなるのでは。
leapyear(と commonyear )を連呼しているだけにも思えますが……。
説明変数に private や final を指定しているのは、
- どこか別からの不要なアクセスを禁じる
- 不要な再代入を禁じる
ためです。
これも大事な習慣づけ。
以下2冊の参考書籍でもこの重要さに触れていました。
参考文献:改訂新版 良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方
参考文献:リファクタリング 既存のコードを安全に改善する(第2版)
このおかげで、
「この関数は何だ?うるう年(leapyear)を返すのか」
「じゃあ、この関数はうるう年かどうか判定しているんだな」
と、気づくきっかけを作れた……と思います。
⑦さらにリファクタリング
いくらか期間を空けて、再度見直ししました。
修正箇所を3つ見つけました。
- 関数名を calculate → isLeapYear に
- クラス名を LeapYears → LeapYear に
- 最初の2つのテストを1つに合わせる
1・2点目はChatGPTに相談した結果です。
Java(に限った話ではないかも)では、
boolean 値を返す関数は is……, has……, can…… のどれかとするのが通例という回答。
isLeapYear(2024) って読むと、
「2024年はうるう年か?」って、そのまま英語として自然に読める。
また、
1年を受け取って boolean を返すなら → isLeapYear。
もし複数年まとめて判定して List<Boolean> とか返す関数なら、
Yearsも検討できるけど。
(ChatGPTの回答より)
という至極もっともな説明。
そして3点目ですが、
最初のテスト2つ(うるう年か平年かを判定する)は分別の必要がないと感じ、
1つにまとめました。
@Test
@DisplayName("isLeapYear()のテスト:入力値がうるう年(4で割り切れる数字)ならtrueを返す")
void testOfLeapYear() {
LeapYear LY = new LeapYear();
int leapyear = 2004;
int commonyear = 2005;
assertTrue(LY.isLeapYear(leapyear));
assertFalse(LY.isLeapYear(commonyear));
}
他にもリファクタリングできる個所はあるかもしれないですが、
とりあえずここで完了としました。
書籍を読み直したり、他の開発も続けてみたいと思います。