『Software Design 2024年2月号』に寄稿したテストの考え方を用いた、具体的なテストの改善例

はじめに

先日、私が第1特集「新しいソフトウェアテスト講座」の第1章「ソフトウェアテストとは何か?」を寄稿した、『Software Design 2024年2月号』が発売されました。

gihyo.jp

この雑誌を読んだ黒柴さんが、雑誌の第1特集をキッカケとして*1ブログ記事を書いてくださりました。ありがとうございます!

note.com

そこで、本記事では、上記の記事に載っている題材について、第1特集の第1章でお伝えした内容を活用して、改善案を書いてみたいと思います。

目次

黒柴さんのブログ記事の概要

参考元にしている黒柴さんのブログ記事では、以下のような題材を扱っていました。

仕様:
ある工場では、3直で24時間勤務を行っており、アプリの画面には現在の直で実施予定の作業情報を表示する
そのため、テスト対象の処理ルーチンでは、現在時刻を元に現在の直の開始時刻を算出する仕様としている
各直の勤務時間は以下のとおりとする

直ごとの勤務時間
テストケース:
担当者(実装、およびユニットテストを担当)が考えたテストケースは、以下のようなものだった
誤ったテストケース
このテストケースは、直の仕様を以下のようなパーティション分割であると整理して作成したものと予想される
テストケースの元にした直のパーティション
不具合の詳細:
上記のテストケースでは、日付が考慮されていない
そのため、項番4の7:29では前日の23:00を直の開始時刻とするべきところを、時刻のみを計算して直の開始日時としているため、当日の23:00を開始時刻として出力していた
この点では、対象の処理ルーチンが「直の開始日時」ではなく、「直の開始時刻」を算出していることに問題がある

提示された仕様では、直の情報に日付が定義されていないが、時刻のみならず日付も考慮したうえで、以下のように整理されるべきであると考える

日付を考慮した直のパーティション

テストプロセスを踏まえて考え直す

Software Design 2024年2月号』の私の寄稿の中で示している通り、個人的にはテストプロセスを踏まえた方が良いと思っています*2

ISTQBテスト技術者資格制度 Foundation Level シラバス 日本語版 Version 2023V4.0.J01(以下、JSTQB)で示しているテストプロセス

なので、まずはテスト分析から考えてみます。

テスト分析

今回の場合、「何をテストしたいのか」という考え(これを「テスト条件」と言います)でいくと、以下のような部分が気になります。

  • a)現在の勤務に応じて表示が変わるのか
    • 例えば、現在時刻が二勤の時間帯の場合は、"15:30"という表示になるのか
  • b)勤務が被っている部分についてはどのように表示されるのか
    • 例えば、現在時刻が15:31の場合は、一勤の時間帯なのか、それとも二勤の時間帯なのか
  • c)日付を跨いだ場合、適切に表示されるのか
    • 例えば、現在時刻が02:00の場合は、"00:00"とはならずに、"23:30"という表示になるのか
  • d)休憩時間中の場合、適切に表示されるのか
    • 例えば、現在時刻が12:00の場合は、"07:30"という表示になるのか

このうち、a)は元々行われていたテスト条件、c)は発生した不具合で漏れていたテスト条件となります。また、b)とd)は私が必要だと感じたテスト条件です。個人的には特にb)に不具合が潜んでいそうだなと感じています。

続いて、これらのテスト条件に対してのテスト設計技法の適用を考えます。

今回は時刻について、a)とb)についての同値分割および境界値分析と、c)についての同値分割および境界値分析と、d)についての同値分割および境界値分析を行うのが適切だと考えました。なぜ、a)b)とc)とd)で分けたかというと、一緒くたに表現すると、何をテストしようとしているのか見通しが悪くなると感じたからです。

現在の勤務に応じた表示に関するテスト設計およびテスト実装

テスト設計(同値分割)

以下のように同値分割します。

現在の勤務についての同値分割をした結果

同値分割で大事なのは、複数のパーテーションに属することがないようにすることです。

JSTQBシラバスでも以下のように書かれています。

パーティションは、重複してはならず、空でない集合でなければならない。

そのため以下のような表現は、同値分割法で表現できていないことになります*3

同値分割の考え方にそぐわない表現方法

テスト設計(境界値分析)

同値分割を行った結果を用いて、境界値分析を考えると、以下のようになります。

現在の勤務についての境界値分析をした結果(同値分割の※1周辺のみを抜粋)

今回の図では※1周辺のみを示しましたが、※2、※3周辺も同様に表現できるはずです。

テスト実装

テスト設計で明らかになった境界値を考慮して作成したテストケースは以下のようになります。

境界値を考慮して作ったテストケース

このうち、「???」とした部分は、元々の記載内容だけでは判断できなかった部分です。この部分こそ、チームで議論し、明らかにすべき箇所です。

このように、プログラムを1文字も書かなくてもテストを考えることができますし、場合によってはテスト実行をせずとも、テスト実装以前の段階で不具合を発見することができます

2024/02/06追記

元々の記載内容だけでは判断できなかった部分について、黒柴さんに補足していただきました。ありがとうございます!

日付を跨いだ場合の表示に関するテスト設計およびテスト実装

テスト設計

前述までと同様、同値分割を行います。なお、勤務が被っている部分については、前述のテスト設計を元に議論した結果、後続の勤務グループの時間として判断したとします。

日付を考慮して同値分割をした結果

境界値分析の図示は、前述と同様の表現となるため割愛します。

テスト実装

テスト設計で明らかになった境界値を考慮して作成したテストケースは以下のようになります。

境界値を考慮して作ったテストケース

ここまでと同様に、休憩時間中の表示に関するテスト設計およびテスト実装も行うことができるはずです。今回は説明を割愛します。

テストコードの表現への活用

さて、ここまで整理してきたテスト設計やテスト実装の情報は、テストコードに活かすことができないのでしょうか。

私は活かすことができると考えます。テストメソッド名やテストコードの構造化に役立てることができます。

元記事に書いてあったテストケースの表は以下のようになります。

テストケースの表

これをそのままテストコードで表現してしまうと、見通しが少し悪い(保守性が悪い)テストコードができてしまう可能性があります。少なくとも、数年後にこのテストケースを見た時に、どんな目的で書かれたテストなのかが分からなくなっているでしょう。

一方、今回のテスト分析で示したように、a)、b)、c)、d)で分けて表現するといかがでしょう。例えば、以下のようになります*4

public class DisplayDateTimeTest {

    @Nested
    @DisplayName("現在の勤務グループに応じた表示のテスト")
    class JudgeGroupTest {

        @Test
        @DisplayName("一勤と判断される時間帯のテスト")
        void judge_first_group(){
            assertEquals(displayDateTime("2024/02/05 13:00"), "2024/02/05 07:30");
        }

        @Test
        @DisplayName("二勤と判断される時間帯のテスト")
        void judge_second_group(){
            assertEquals(displayDateTime("2024/02/05 17:00"), "2024/02/05 15:30");
        }

        @Test
        @DisplayName("三勤と判断される時間帯のテスト")
        void judge_third_group(){
            assertEquals(displayDateTime("2024/02/05 23:45"), "2024/02/05 23:30");
        }
    }

    @Nested
    @DisplayName("勤務が複数被っている付近の時間帯のテスト")
    class DuplicateGroupTest {

        @Test
        @DisplayName("一勤の勤務時間と判断される最後の時刻のテスト")
        void judge_first_group_end_boundary_time(){
            assertEquals(displayDateTime("2024/02/05 15:29"), "2024/02/05 07:30");
        }

        @Test
        @DisplayName("二勤の勤務時間と判断される最初の時刻のテスト")
        void judge_second_group_start_boundary_time(){
            assertEquals(displayDateTime("2024/02/05 15:30"), "2024/02/05 15:30");
        }

        @Test
        @DisplayName("二勤の勤務時間と判断される最後の時刻のテスト")
        void judge_second_group_end_boundary_time(){
            assertEquals(displayDateTime("2024/02/05 23:29"), "2024/02/05 15:30");
        }

        @Test
        @DisplayName("三勤の勤務時間と判断される最初の時刻のテスト")
        void judge_third_group_start_boundary_time(){
            assertEquals(displayDateTime("2024/02/05 23:30"), "2024/02/05 23:30");
        }

        @Test
        @DisplayName("三勤の勤務時間と判断される最後の時刻のテスト")
        void judge_third_group_end_boundary_time(){
            assertEquals(displayDateTime("2024/02/06 07:29"), "2024/02/05 23:30");
        }

        @Test
        @DisplayName("一勤の勤務時間と判断される最初の時刻のテスト")
        void judge_first_group_start_boundary_time(){
            assertEquals(displayDateTime("2024/02/06 07:30"), "2024/02/05 23:30");
        }
    }

    @Nested
    @DisplayName("日付を跨いだ場合のテスト")
    class AcrossDaysTest {

        @Test
        @DisplayName("当日の三勤の勤務時間と判断される最後の時刻のテスト")
        void judge_today_third_group_end_boundary_time(){
            assertEquals(displayDateTime("2024/02/05 23:59"), "2024/02/05 23:30");
        }

        @Test
        @DisplayName("前日の三勤の勤務時間と判断される最初の時刻のテスト")
        void judge_yestarday_third_group_start_boundary_time(){
            assertEquals(displayDateTime("2024/02/06 00:00"), "2024/02/05 23:30");
        }
    }
}

テストコードのさらなる改善

上記のコードだと「どの勤務グループなのか」と「勤務グループの開始時刻はいつなのか」の2つの確認を合わせて行っています。そこで、さらにテストコードを改善してみます。

public class DisplayDateTimeTest {

    @Nested
    @DisplayName("現在の勤務グループの判定テスト")
    class JudgeGroupTest {

        @Test
        @DisplayName("一勤と判断される時間帯のテスト")
        void judge_first_group(){
            assertEquals(judgeGroup("2024/02/05 13:00"), "一勤");
        }

        @Test
        @DisplayName("二勤と判断される時間帯のテスト")
        void judge_second_group(){
            assertEquals(judgeGroup("2024/02/05 17:00"), "二勤");
        }

        @Test
        @DisplayName("三勤と判断される時間帯のテスト")
        void judge_third_group(){
            assertEquals(judgeGroup("2024/02/05 23:45"), "三勤");
        }
    }

    @Nested
    @DisplayName("勤務が複数被っている付近の時間帯の勤務グループの判定テスト")
    class DuplicateGroupTest {

        @Test
        @DisplayName("一勤の勤務時間と判断される最後の時刻のテスト")
        void judge_first_group_end_boundary_time(){
            assertEquals(judgeGroup("2024/02/05 15:29"), "一勤");
        }

        @Test
        @DisplayName("二勤の勤務時間と判断される最初の時刻のテスト")
        void judge_second_group_start_boundary_time(){
            assertEquals(judgeGroup("2024/02/05 15:30"), "二勤");
        }

        @Test
        @DisplayName("二勤の勤務時間と判断される最後の時刻のテスト")
        void judge_second_group_end_boundary_time(){
            assertEquals(judgeGroup("2024/02/05 23:29"), "二勤");
        }

        @Test
        @DisplayName("三勤の勤務時間と判断される最初の時刻のテスト")
        void judge_third_group_start_boundary_time(){
            assertEquals(judgeGroup("2024/02/05 23:30"), "三勤");
        }

        @Test
        @DisplayName("三勤の勤務時間と判断される最後の時刻のテスト")
        void judge_third_group_end_boundary_time(){
            assertEquals(judgeGroup("2024/02/06 07:29"), "三勤");
        }

        @Test
        @DisplayName("一勤の勤務時間と判断される最初の時刻のテスト")
        void judge_first_group_start_boundary_time(){
            assertEquals(judgeGroup("2024/02/06 07:30"), "一勤");
        }
    }

    @Nested
    @DisplayName("日付を跨いだ場合のテスト")
    class AcrossDaysTest {

        @Test
        @DisplayName("当日の三勤の勤務時間と判断される最後の時刻のテスト")
        void judge_today_third_group_end_boundary_time(){
            assertEquals(displayDateTime("2024/02/05 23:59"), "2024/02/05 23:30");
        }

        @Test
        @DisplayName("前日の三勤の勤務時間と判断される最初の時刻のテスト")
        void judge_yestarday_third_group_start_boundary_time(){
            assertEquals(displayDateTime("2024/02/06 00:00"), "2024/02/05 23:30");
        }
    }
}

このようにすることで、もしもテストがNGになった場合にも、どんなテスト条件に関わる部分(何についてのテスト)なのかわかりやすい形になり、保守性が上がります。

また、例えば「二勤の開始時刻が1530→15:00」と変更された(仕様が変わった)としても、変えるべきテストは「一勤の勤務時間と判断される最後の時刻のテスト」と「二勤の勤務時間と判断される最初の時刻のテスト」だと、テストメソッド名だけを見て予想を立てることができます

このように、テストコードの作成前に(特にテスト分析とテスト設計で)行うべきテストを整理することで、保守性の高いテストコードを書くことができます

おわりに

今回は、黒柴さんの記事を元に、テストプロセスのテスト分析〜テスト設計〜テスト実装、そしてテストコードの改善について考えてみました。

本記事では1つのケースを取り上げてみましたが、考え方については『Software Design 2024年2月号』に寄稿という形で書きましたので、よければそちらもご覧ください。

gihyo.jp

*1:

*2:JSTQBの中では「テスト計画」「テストのモニタリングとコントロール」「テスト完了」もありますが、今回は単一のテストについてなので割愛します

*3:同値分割法で表現できていないだけであって、仕様を理解する図としてはあっても良いと思っています

*4:テストコードに残すにあたって、同値分割のみにしたり、重複している境界値のテストケースは削ったりといった小さな工夫をしていますが、今回は説明を割愛します