とあるお兄さんの雑記

基本的に技術系の内容を書きますが、何を書くかは私の気分です。

見よう見まねでテスト駆動開発〜その1〜

テスト駆動開発ですよ、お兄さん(特に意味はない)。

テスト駆動開発とは?

世の中には色々な開発手法があるのですが、開発手法の一つにテスト駆動開発というものがあります。テスト駆動開発(Test-Driven Development: TDD)というのは文字通りテストを最初に(テストファーストで書いていくものです。

普通であれば、作りたい機能を作ってから、その機能がちゃんと動いているのかという自動化テストを書いていきます。
一方、テスト駆動開発作りたい機能のテストを先に書いて、開発を進めていく開発スタイルです。具体的な手順は後々見ていくとして、テスト駆動開発のルールとしては2つです。

  1. 自動化されたテストが失敗した時のみ、新しいコードを書く
  2. 重複を除去する

以上2つのみです。シンプルですね。

ちなみに、テスト駆動開発有名な方だと和田卓人さんという方がいらっしゃいます。
また、以下の本も有名です。私はTDDが気になっていたということもあり、買いました。

テスト駆動開発

TDDに取り組もうとしたきっかけ

会社で開発時、私が全くと言っていいほどテストを書かなかったためです。まぁ、学生の頃ユニットテストの「ユ」の字も知らなかったので仕方ないといえば仕方ないのですが。

ひとまず、先輩に言われてから少しずつテストを書くようになったのはいいのですが、せっかくだしテストについて勉強してみようと思い、前々から気になっていたTDDを学んでみようと思ったわけです。

実際にやってみて

で、上記の本(テスト駆動開発)を買った上で、TDDをやってみたはいいんですが、いつの間にか本に書いてあることを理解するより、本を読み進めることを目標にしていました...。そのため、第1部を読み切ったはいいのですが、全く理解していないという状況に陥ってしまいました。テストを学びたいという気持ちはどこへやら....。

流石にこれだと学んだことが実際に活かしにくいので、それならもうちょっと簡単な例でテスト駆動開発(もどき)をやってみよう!と思い立ったので実際にやってみることにします。


※今回の記事では、一応TDDのスタイルである機能を作っていくことにしますが、実際の現場ではTDDのルール全てに従う必要はないかなと思います。そこら辺は臨機応変にいきましょう。

環境

macOS BigSur メモリ16G
Java 11.0.9.1
JUnit5 
IntelliJ IDEA CE 2020.3

今回作成するもの

今回作成するものは、Javaを一通り学んだ(とは言ってもメソッドを学んだぐらいの段階ですが)大学生の私が深夜テンションで作った、西暦と月と日にちを与えればその日の曜日を計算できる機能です。
(勘の良い方は元ネタが何かわかるかと思います。サマーウォーズの序盤の序盤で出てきた奴です。「サマーウォーズ モジュロ演算」とかでググれば出てきます。)

今回作るこの機能を、この記事ではモジュロ演算システムと呼ぶようにし、テスト駆動で開発してみることにします。

テスト駆動開発の順序

  1. レッド:動作しない、おそらく最初のうちはコンパイルも通らないテストを1つ書く
  2. グリーン:そのテストを迅速に動作させる。このステップでは罪を犯しても良い
  3. リファクタリング:テストを通すために発生した重複を全て除去する

ここでリファクタリングというのは、外部から見た振る舞いを変えずに、コードを書き換えて改善することです。この順序を元にモジュロ演算システムを作ってみましょう。

モジュロ演算システムを作る

TODOリスト

それでは、実際にモジュロ演算システムをTDDで作っていきましょう。
と、その前に、一旦ゴールを明確にしましょう。

テスト駆動開発ではTODOリストを作成して、何に取り組めば良いかを視覚化しているので、まずはこの本に乗っ取ってTODOリストから作成してみることにします。

TODOリスト


  • 西暦、月、日にちを入力として与えると、その曜日を出力する


まぁ、上記でもいいのですが、もうちょっと具体的に書いてみましょう。
例えば、2021年4月1日の曜日は木曜日ですので、以下のようにTODOリストを書き換えてみます。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する


これが、TDDとして正しいかどうかはプロの方に実際に見てもらうとして、目標としては具体的になりました。とは言っても、まだこれだと内部でどうやって計算すれば良いのか分からないので、複雑に感じます。
ここで、実際に求め方の過程を見てみることにします。参考にする記事はこちらです。

ameblo.jp

上記記事を参考にすると、

  1. 西暦、月、日にちを受け取る(例. 2021年4月1日)
  2. 西暦の千の位と百の位(2021なので、上2桁の20)を J、西暦の下2桁(2021なので、21)を K、月を m、日にちを q、曜日を hとする。
  3. ただし、求めたい日の月が1月の場合は前年の13月、2月の場合は14月とする。(仮に2021年の1月1日なら、2020年の1月1日として計算)
  4. チェラーの公式(ツェラーの公式)(後述)を使って、 hを計算する。
  5. 計算した hの値と曜日の対応は以下の通りである。
 h 曜日
0 土曜日
1 日曜日
2 月曜日
3 火曜日
4 水曜日
5 木曜日
6 金曜日

ここで、チェラーの公式(ツェラーの公式)を簡単に紹介します。

 \displaystyle h = \left( q + \left\lfloor \frac{26(m+1)}{10} \right\rfloor  + K  + \left\lfloor \frac{K}{4} \right\rfloor + \left\lfloor \frac{J}{4} \right\rfloor  -2J \right) \bmod   7

ここで、 \displaystyle  \left\lfloor N \right\rfloor   Nを超えない最大の整数(床関数と呼ばれる)、 \displaystyle x \bmod n x nで割った剰余(余り)を表します。

また、床関数についてですが、例えば \displaystyle  \left\lfloor 5.8 \right\rfloor  だった場合、 \displaystyle  \left\lfloor N \right\rfloor   Nを超えない最大の整数であるため、 \displaystyle  \left\lfloor 5.8 \right\rfloor  の計算結果は整数になります。
さらに、 Nを超えない最大の整数であることから、 5.8を超えない最大の整数を求めることになります。つまり、 5.8以下の整数( 5, 4, 3, \dots )で最大の数字は 5となるため、今回でいえば 5となります。

これで求める過程がわかったので、TODOリストに書き込んでいきましょう。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

おおよそ、こんなものでいいでしょう。一旦ここはこのままで進めていくとし、もし、必要なものが出てくればその都度TODOリストに追加していくとしましょう。

コラム:チェラーの公式を使って求めてみる。

簡単に紹介したところで、チェラーの公式に実際に値を代入して計算してみます。2021年4月1日を例に計算します。

 J = 20 K = 21 m = 4 q = 1より、

 \displaystyle \left( q + \left\lfloor \frac{26(m+1)}{10} \right\rfloor  + K  + \left\lfloor \frac{K}{4} \right\rfloor + \left\lfloor \frac{J}{4} \right\rfloor  -2J \right)

 \displaystyle  = \left( 1 + \left\lfloor \frac{26(4+1)}{10} \right\rfloor  + 21  + \left\lfloor \frac{21}{4} \right\rfloor + \left\lfloor \frac{20}{4} \right\rfloor  -2 \times 20 \right)

 \displaystyle  = \left( 1 + \left\lfloor 13 \right\rfloor  + 21 + \left\lfloor 5.25 \right\rfloor + \left\lfloor 5 \right\rfloor  -40 \right)

 \displaystyle  = 1 + 13  + 21  +5 + 5 - 40

 \displaystyle  =  45 - 40

 \displaystyle  =  5

よって、

 \displaystyle  h =  5 \bmod 7

 \displaystyle  h =  5

 \displaystyle  h =  5より、2021年4月1日は木曜日となることがわかり、実際のカレンダーをみても一緒だということがわかりました。

TODOリストを参考にテストを書く

現在のTODOリストは以下のようになっています。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

ここで、TODOリストから一つ取り出して作業を行っているものは太字で表すことにします。まずは「2021年の上二桁を取得」をとってくることにします。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

レッド

では、実際にテストを書いていきましょう。
TDDはなんと言っても、テストファーストです。どんなオブジェクトが必要かなんて考えることなく、以下のようにテストから先に書きます。

ModuloTest.java

package modulo;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ModuloTest {
    @Test
    public void testFirst2Digits(){
        Modulo modulo = new Modulo();
        assertEquals(20, modulo.getFirst2Digits(2021));
    }
}

TDDを初めて学ぶ人からするとかなり面食らうと思います。というのも、Moduloというクラスも何もないのですから(私もびっくりしました)。ですが、これがTDDです。
TDDの順番として、

  1. レッド:動作しない、おそらく最初のうちはコンパイルも通らないテストを1つ書く
  2. グリーン:そのテストを迅速に動作させる。このステップでは罪を犯しても良い
  3. リファクタリング:テストを通すために発生した重複を全て除去する

レッドの部分をやったことになります。

グリーン

というわけで次にグリーンを行います。
まず必要なのはModuloというクラスを作ることです。

Modulo.java

package modulo;

public class Modulo {
}


次に必要なのはgetFirst2Digitsメソッドです。しかも引数をもらい、戻り値のあるものなので、以下のように書きます(念の為、空のコンストラクタも書いておきましょう)。

Modulo.java

package modulo;

public class Modulo {

    public Modulo(){

    }
    
    public int getFirst2Digits(int year){
        return 0;
    }
}

これでエラーが無くなったので、テストを走らせてみます。

f:id:kurasher:20210330001028p:plain

エラーが出てきました。まぁ、当たり前ですが。さて、このテストをできるだけ早く通さないといけません。そのためには、罪を犯しても良いので、以下のように書きます(私は、罪を犯すの意味が今のところまだ理解できておりません)。

Modulo.java

package modulo;

public class Modulo {

    public Modulo(){
    }

    public int getFirst2Digits(int year){
        return 20;
    }
}

f:id:kurasher:20210330001554p:plain

今度はテストが通りましたので、ひとまず手順2のグリーンは突破しました。

リファクタリング

グリーンを突破したので、今度はリファクタリングが必要です。

本来Modulo.javaクラスの中にあるgetFirst2Digitsメソッドは西暦の上2桁を取得するものでした。というわけで、getFirst2Digitsのメソッドから上2桁を取得する処理を追加し、戻り値の20を削除することにします。

Modulo.java

package modulo;

public class Modulo {

    public Modulo(){
    }

    public int getFirst2Digits(int year){
        year = year / 100;
        return year;
    }
}

上記のように書いてみました。一応、上記でもテストは通るのですが、getFirst2Digitsメソッドは西暦の上2桁を first2Digitsという変数で返したほうがわかりやすいように思えます(個人の感覚になりますので、無理に合わせる必要はないですし、そういう考えもあるかぐらいに流していただければと思います)。

というわけで、 first2Digitsという変数をModulo.javaに追記します。ついでに、コンストラクタで初期化しておきます。

Modulo.java

package modulo;

public class Modulo {
    private int first2Digits;

    public Modulo(){
        first2Digits = 0;
    }

    public int getFirst2Digits(int year){
        first2Digits = year / 100;
        return first2Digits;
    }
}

これで、だいぶ見栄えがよくなりましたね!
以上で、リファクタリングの手順は終了です。TODOリストも更新しておきましょう。終わった作業に関しては取り消し線で表すことにします。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

次回

今回は長くなりましたので、この辺で終了です。
思ったよりもTDDの概要や、余計な話で持っていかれてしまったので、次回以降、より本格的にTDDでモジュロ演算システムを作成していきます。