0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

テスト駆動開発で初心者向けエクササイズ(うるう年判定)やってみた

Posted at

この記事に書かれていること

  • Java初級者が
  • テスト駆動開発で
  • うるう年判定機能を開発してみた

書籍:テスト駆動開発 に則った手順で開発し、その流れを再現しました。
参考文献:テスト駆動開発

テストコード&プロダクションコードを記載しました。
「こうした方がいいよ」といった助言歓迎です。

開発環境

  • Eclipse: 2023-09 (4.29.0)
  • OS: Windows 11
  • Java: 17.0.9
  • JUnit5

書かれていないこと

  • テスト駆動開発の詳細な説明
  • 環境設定手順

書籍読者もしくは、既にテスト駆動開発をご存じの方向けです。
あくまで復習として書いた記事です。

テストコード完成形

テストコード
LeapYearTest.java
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));
	}
}

プロダクトコード完成形

プロダクトコード
LeapYear.java
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 文を使えば実装できると判断。

まずは

  • テストクラスの作成
  • 実装すべき機能のメモ

にとりかかりました。

LeapYearsTest.java
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 を返してほしい

これらを確かめてくれるテストを書いてみました。

LeapYearsTest.java
class LeapYearsTest {
    @Test
	@DisplayName("calculate()のテスト:入力値がうるう年(4で割り切れる数字)ならtrueを返す")
	void testCalculateOfLeapYear() {
		LeapYears LY = new LeapYears();
		int leapyear = 2004;
		assertTrue(LY.calculate(leapyear));
	}
}

判定する関数は calculate という名前が適切なのだろうか?
とりあえず仮置きしました。
テストを実行すると、レッドバーが出ました。これでよし。

③テストをクリアする実装に移る

テストは合格しなければいけないので、ここで初めて実装します。
該当のクラスを作り、関数作成。
ただ、仮実装段階なのでまだ作り込みません。
true を返すだけ。

LeapYears.java
class LeapYears {
	boolean calculate(int year) {
		return true;
	}
}

テストを実行してみたところ、グリーンバー。やった。

④リファクタリング

遠回りな気もしますが、テスト駆動開発の手順通りです。

  • テスト作成
  • (仮実装 &) 開発
  • リファクタリング

ただでさえ私はプログラミングに慣れていないので、
横着せずにこの方針に則りました。

現状だと、どんな年を渡してもうるう年判定されてしまいます。
そのため、

  • テストをクリアしつつ
  • 適切な処理ができるように

修正します。

LeapYears.java
class LeapYears {
	boolean calculate(int year) {
		if (year % 4 == 0) return true;
		return false;
	}
}

テストを実行してみたところ、変わらずグリーンバー。
と、ここで気づきました。
平年のときは平年と判定してもらわないと困る!
調べるテストを追加。

LeapYearsTest.java
    @Test
	@DisplayName("calculate()のテスト:入力値が平年(4で割り切れない数字)ならfalseを返す")
	void testCalculateOfPlainYear() {
		LeapYears LY = new LeapYears();
		int commonyear = 2005;
		assertFalse(LY.calculate(commonyear));
	}

再度テスト実行。グリーンバーでした。
これで機能1つ目は達成。

  • 西暦年が4で割り切れる年は(原則として)うるう年
  • ただし、西暦年が100で割り切れる年は(原則として)平年
  • ただし、西暦年が400で割り切れる年は必ずうるう年

⑤テストと開発を続ける

次の機能の実装に移ります。
テストが先だと言い聞かせつつ。

  • ただし、西暦年が100で割り切れる年は(原則として)平年

これを満たすかどうかを調べられる、新しいテストコードを作ります。
一つ目のテストを再利用して、
与える year の値を変えるだけで済みそう。

LeapYearsTest.java
    @Test
	@DisplayName("calculate()のテスト:入力値が100で割り切れる(平年)ならfalseを返す")
	void testCalculateOf100Year() {
		LeapYears LY = new LeapYears();
		int hundredyear = 100;
		assertFalse(LY.calculate(hundredyear));
	}

テストを実行してみると、レッドバー。
今のコードだと、100も4で割り切れてうるう年と判定されるため。
修正します。

LeapYears.java
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 文を追加するだけで大丈夫そう……おっと危ない。テストが先

LeapYearsTest.java
    @Test
	@DisplayName("calculate()のテスト:入力値が400で割り切れる(うるう年)ならtrueを返す")
	void testCalculateOf400Year() {
		LeapYears LY = new LeapYears();
		int four_hundredyear = 400;
		assertTrue(LY.calculate(four_hundredyear));
	}

テストを実行すると、またレッドバー。想定通り。
コードを修正。

LeapYears.java
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;
	}
}

テストを実行すると、グリーンバー。
これでよし。

⑥またリファクタリング

それでは再度リファクタリング。
と言っても、どこを直せばいいんだろう?

LeapYears.java
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 の部分がちょっと不親切かもしれない。
これを、いわゆる説明変数にしてしまえばどうだろう。
こんな感じに。

LeapYears.java
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 )を連呼しているだけにも思えますが……。

説明変数に privatefinal を指定しているのは、

  • どこか別からの不要なアクセスを禁じる
  • 不要な再代入を禁じる

ためです。
これも大事な習慣づけ。
以下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つにまとめました。

LeapYearTest.java
    @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));
	}

他にもリファクタリングできる個所はあるかもしれないですが、
とりあえずここで完了としました。
書籍を読み直したり、他の開発も続けてみたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?