TDDの考え方を活用してレガシーコードのリファクタリングに立ち向かう

はじめに

本記事はテスト駆動開発 Advent Calendar 2020 最終日の記事です。このアドベントカレンダーはスカスカなので、今からでもテスト駆動開発経験談などをエントリーしてもらえると嬉しいです!

目次

レガシーコードのリファクタリング

テスト駆動開発(以下、TDD)を学び始めた人が、現場でぶつかる壁の一つは「レガシーコード*1なのでTDDが使えない」だと思います。

「レガシーコードをリファクタリングしたいけど、リファクタリングするためのテストコードがない」「テストコードを書きたいけど、レガシーコードなのでテストコードが書きづらい」というジレンマに陥りがちです。

そこで本記事では、TDDの考え方を活用しつつ、まずは1つだけでもテストコードを注入することで、レガシーコードに立ち向かっていきます。*2

今回の題材

レガシーコードになりがちなコードの1つとして、現在時刻に依存しているなど、テストしづらい状況であるコードがあります。

例えば、以下のようなコード*3です。

//DeliveryDate.java
import java.time.LocalDate;
import java.time.Month;

public class DeliveryDate {
    public LocalDate getDeliveryDate(){
        LocalDate localDate = LocalDate.now();
        int day = localDate.getDayOfMonth();
        Month month = localDate.getMonth();
        int year = localDate.getYear();

        if(day >= 25){
            month.plus(1L);
        } else if (month.equals(Month.DECEMBER) && day >= 20) {
            month.plus(1L);
        }

        int lastDay;
        if(month.equals(Month.APRIL)) {
            lastDay = 30;
        } else if(month.equals(Month.JUNE)){
            lastDay = 30;
        } else if(month.equals(Month.SEPTEMBER)){
            lastDay = 30;
        } else if(month.equals(Month.NOVEMBER)){
            lastDay = 30;
        } else if(month.equals(Month.FEBRUARY)){
            if(year%4 == 0){
                lastDay = 29;
            } else {
                lastDay = 28;
            }
        } else {
            lastDay = 31;
        }
        return LocalDate.of(localDate.getYear(),
            localDate.getMonth(), lastDay);
    }
}

このコードは、配送日を指定するコードです。6行目で現在日時を挿入し、その日時の月末を配送日として設定します。しかし、現在日時が下旬や年末の場合は、配送日が次月に設定されます。

本章ではこのコードに対してリファクタリングを行っていきます。

最初のテストコード

さて、このようなコードに対して、どのようにテストケースを追加していけば良いでしょうか。

最初に作るべきテストコードでは「とりあえず動くテスト」を目指します。

今回の場合は以下のようなコードをとりあえず作ってみましょう。

//DeliveryDateTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class DeliveryDateTest {

    @Test
    void _配送日のテスト() {
        new DeliveryDate(); //DeliveryDateクラスを呼び出す
        assertEquals(1,1);
    }
} 

ただ、DeliveryDateクラスを呼び出しただけ*4です。

この状態でテスト実行をしてみましょう。もしもテスト実行してRedになった*5場合、以下の2つのどちらかが原因でしょう。

  • テストフレームワークの設定自体が間違っている
  • DeliveryDateクラスの呼び出しの際に必要な設定が足りない

このうち、他のクラスで同じテストフレームワークを用いて動いていた場合は、1つ目の原因の可能性は限りなく低いでしょう。つまり、今回のテストコードを実行することで、そもそも実装コードを実行できるのか確認することができます*6

テスト実行してGreenになった場合、次はメソッドを呼び出します。

//DeliveryDateTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class DeliveryDateTest {

    @Test
    void _配送日のテスト() {
        new DeliveryDate().getDeliveryDate(); //getDeliveryDateメソッドを実行する
        assertEquals(1,1);
    }
}

最初に書いたテストコードと同様、これもgetDeliveryDate()メソッドを実行できるのか確認することができます。

このようにテストコードを書くことで、実装コードを手軽に試すことができます。しかも一度書くと、自動で何度も実行することができます。

JaSST'18 Tokyo 招待講演*7 で、柴田芳樹さんは下記の発言をしていますが、今回の過程を写経すると実感ができると思います。

Unitテスト作成は自動でデバッグしている感覚

TDDは常に実装する感覚

仕様を理解してテストを作る

次は実装コードを見ながら期待値を当てはめていきます。

//DeliveryDateTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class DeliveryDateTest {

    @Test
    void _配送日のテスト() {
        LocalDate actualDate = new DeliveryDate().getDeliveryDate();
        assertEquals(LocalDate.of(2020,9,30),actualDate); //期待値を変更する
    }
}

今回の場合、getDeliveryDateメソッドを呼び出すと、今日の日付を元に月末の日付などが返ってきます。このファイルを作成した日付が2020年9月13日だったため、月末である2020年9月30日を期待値として設定しました。

別のテストケースを作る

続いて、実装コードを見て、別のテストケースを作ってみましょう。

//DeliveryDateTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class DeliveryDateTest {

    @Test
    void _小の月の月末になる場合のテスト() {
        LocalDate actualDate = new DeliveryDate().getDeliveryDate();
        assertEquals(LocalDate.of(2020,9,30),actualDate);
    }

    //大の月のテストケースを追加
    @Test
    void _大の月の月末になる場合のテスト() {
    LocalDate actualDate = new DeliveryDate().getDeliveryDate();
    assertEquals(LocalDate.of(2020,10,31),actualDate);
    }

}

小の月の月末のテストケースを元々作っていたので、大の月の月末の場合のテストケースも作成しようと考えました。

しかし、両方のテストケースにおけるDeliveryDateクラスの呼び出し及びgetDeliveryDateメソッドの呼び出しに違いがない(パラメータの設定などを行っていない)ため、これら2つのテストケースを実行しようとすると、必ずどちらかのテストがRedになります。

つまり、この方法だとうまくいかないことが分かります。このような場合、どうすれば良いのでしょうか。

依存関係を見つける

今回のテストがうまく行かない理由を考えてみましょう。

//DeliveryDate.java
import java.time.LocalDate;
import java.time.Month;

public class DeliveryDate {
    public LocalDate getDeliveryDate(){
        LocalDate localDate = LocalDate.now(); //現在の時刻を代入
        int day = localDate.getDayOfMonth();
        Month month = localDate.getMonth();
        int year = localDate.getYear();

        if(day >= 25){
            month.plus(1L);
        } else if (month.equals(Month.DECEMBER) && day >= 20) {
            month.plus(1L);
        }

        int lastDay;
        if(month.equals(Month.APRIL)) {
            lastDay = 30;
        } else if(month.equals(Month.JUNE)){
            lastDay = 30;
        } else if(month.equals(Month.SEPTEMBER)){
            lastDay = 30;
        } else if(month.equals(Month.NOVEMBER)){
            lastDay = 30;
        } else if(month.equals(Month.FEBRUARY)){
            if(year%4 == 0){
                lastDay = 29;
            } else {
                lastDay = 28;
            }
        } else {
            lastDay = 31;
        }
        return LocalDate.of(localDate.getYear(),
            localDate.getMonth(), lastDay);
    }
}

今回、テストケースがうまく行かない最大の理由は6行目です。 LocalDate.now()で現在の時刻を入れているため、テスト実行日時に依存してしまうのです。

この依存関係を削除する方法を考えましょう。

依存関係を削除する

日時に関する部分をメソッドとして切り出すことで依存関係を削除します。

実際にリファクタリングを行う手順を細かく区切って説明します。それぞれの手順ごとにテストは常に実行し続けた方が良いでしょう。

作業前の状態

作業前のテストコードの状態を改めて記載しておきます。

//DeliveryDateTest.java
import org.junit.jupiter.api.Test;

import java.time.LocalDate;

import static org.junit.jupiter.api.Assertions.*;

class DeliveryDateTest {

    @Test
    void _小の月の月末になる場合のテスト() {
        LocalDate actualDate = new DeliveryDate().getDeliveryDate();
        assertEquals(LocalDate.of(2020,9,30),actualDate);
    }

/* 大の月のテストケースはコメントアウトする
    @Test
    void _大の月の月末になる場合のテスト() {
        LocalDate actualDate = new DeliveryDate().getDeliveryDate();
        assertEquals(LocalDate.of(2020,10,31),actualDate);
    }
*/
}

なお、テストコードは「_大の月の月末になる場合のテスト」のテスト結果がRedになるため、今回はコメントアウトした状態で作業を始めます。

現在作成済みのテストケースが今回の狙いである依存関係部分の実装ロジックを通っているのか確認する

現在作成済みのテストケースを使って、カバレッジを計測します。実行すると、下記画像のようになります。

f:id:nihonbuson:20201210102003p:plain
カバレッジ計測結果

上記画像のうち、赤囲み部分(今回の狙いの部分である6行目)に注目してください。

行番号(6)の右隣にある色を確認します。ここが緑色だと既存のテストケースが実装ロジックを通っていることを示しています。もしも、ここが赤色だとテストケースが実装ロジックを通っていないことを示しているので、赤色の結果のまま、その行のリファクタリングをするのは止めましょう。

テスト実行に影響があるロジックを切り出す

先ほどのカバレッジ計測の結果、6行目はテストしている部分であることが分かったので、LocalDate localDate = LocalDate.now();の部分をメソッド化します。

//DeliveryDate.java
import java.time.LocalDate;
import java.time.Month;

public class DeliveryDate {
    public LocalDate getDeliveryDate(){
        LocalDate localDate = getNow(); //メソッド化
        int day = localDate.getDayOfMonth();
        Month month = localDate.getMonth();
        int year = localDate.getYear();

        if(day >= 25){
            month.plus(1L);
        } else if (month.equals(Month.DECEMBER) && day >= 20) {
            month.plus(1L);
        }

        int lastDay;
        if(month.equals(Month.APRIL)) {
            lastDay = 30;
        } else if(month.equals(Month.JUNE)){
            lastDay = 30;
        } else if(month.equals(Month.SEPTEMBER)){
            lastDay = 30;
        } else if(month.equals(Month.NOVEMBER)){
            lastDay = 30;
        } else if(month.equals(Month.FEBRUARY)){
            if(year%4 == 0){
                lastDay = 29;
            } else {
                lastDay = 28;
            }
        } else {
            lastDay = 31;
        }
        return LocalDate.of(localDate.getYear(),
            localDate.getMonth(), lastDay);
    }

    //メソッドの作成
    private LocalDate getNow() {
        return LocalDate.now();
    }
}

この時点でテスト実行して、テストがGreenを保っていることを確認します*8

テスト実行に影響があるメソッドにアクセスできる範囲を広げる

テストクラスからもアクセスできるように、privateからprotectedに変更します。

//DeliveryDate.java内のgetNowメソッド
    protected LocalDate getNow() {
        return LocalDate.now();
    }

この時点でテスト実行して、Greenを保っていることを確認します。

テスト実行に影響があるメソッドをテストクラス内でOverrideする

まず、テストクラス内にあるDeliveryDateクラスを変数化します。変数化してもテスト実行がGreenになることを確認します。

//DeliveryDateTest.java
    @Test
    void _小の月の月末になる場合のテスト() {
        DeliveryDate deliveryDate = new DeliveryDate(); //DeliveryDateクラスの変数化
        LocalDate actualDate = deliveryDate.getDeliveryDate();
        assertEquals(LocalDate.of(2020,9,30),actualDate);
    }

続いて、DeliveryDateクラスをFakeDeliveryDateクラスに変更します。この時点では、コンパイルエラーが発生します。

//DeliveryDateTest.java
    @Test
    void _小の月の月末になる場合のテスト() {
        DeliveryDate deliveryDate = new FakeDeliveryDate(); //FakeDeliveryDateクラスに変更
        LocalDate actualDate = deliveryDate.getDeliveryDate();
        assertEquals(LocalDate.of(2020,9,30),actualDate);
    }

コンパイルエラーの原因となっているFakeDeliveryDateクラスをインナークラスで作成します*9。この時点でテスト実行してGreenになることを確認します。

//DeliveryDateTest.java
    @Test
    void _小の月の月末になる場合のテスト() {
        DeliveryDate deliveryDate = new FakeDeliveryDate();
        LocalDate actualDate = deliveryDate.getDeliveryDate();
        assertEquals(LocalDate.of(2020,9,30),actualDate);
    }

    //FakeDeliveryDateクラスの作成
    private class FakeDeliveryDate extends DeliveryDate {
    }

インナークラスの中身にOverrideしたgetNow()メソッドを記述します。まずは、DeliveryDateクラスにあるgetNow()を継承します。この時点でテスト実行してGreenになることを確認します。

//DeliveryDateTest.java内にあるインナークラスFakeDeliveryDate
    private class FakeDeliveryDate extends DeliveryDate {
        //getNowメソッドを継承する
        @Override
        protected LocalDate getNow() {
            return super.getNow();
        }
    }

続いて、getNow()メソッドの中身を書き換えます。まずは、DeliveryDateクラスにあるgetNow()メソッドの中身をそのままコピペします*10。この時点でテスト実行してGreenになることを確認します。

//DeliveryDateTest.java内にあるインナークラスFakeDeliveryDate
    private class FakeDeliveryDate extends DeliveryDate {
        @Override
        protected LocalDate getNow() {
            //DeliveryDateクラスのgetNow()メソッドの中身をそのままコピペして持ってくる
            return LocalDate.now();
        }
    }

最後に、getNow()メソッドの中身を日付指定の処理に変更します。この時点でテスト実行してGreenになることを確認します*11

//DeliveryDateTest.java内にあるインナークラスFakeDeliveryDate
    private class FakeDeliveryDate extends DeliveryDate {
        @Override
        protected LocalDate getNow() {
            return LocalDate.of(2020,9,13); //日付指定の処理に変更
        }
    }

このように変更していくことで、実行日付に依存しているgetNow()メソッドのみを排除した形でテスト実行できるようにできました。

テストメソッド内で日付指定できるようにする

現在の状態でテスト実行ができるようになりましたが、このままだと必ず2020年9月13日が指定されてしまいます。そこでテストメソッドごとに日付指定できるようにします。

まずは日付指定ができるようなsetDate()メソッドを作成します。この時点ではsetDate()メソッドが存在しないためコンパイルエラーが発生します*12

//DeliveryDateTest.java内にある小の月の月末テスト
    @Test
    void _小の月の月末になる場合のテスト() {
        FakeDeliveryDate deliveryDate = new FakeDeliveryDate();
        deliveryDate.setDate(LocalDate.of(2020,9,13)); //setDate()メソッドの呼び出し。現在はsetDate()メソッドがないためコンパイルエラーが発生
        LocalDate actualDate = deliveryDate.getDeliveryDate();
        assertEquals(LocalDate.of(2020,9,30),actualDate);
    }

次に、setDate()メソッドを実装します。コンパイルエラーが解消され、テスト実行すると再びGreenになります。

//DeliveryDateTest.java
    @Test
    void _小の月の月末になる場合のテスト() {
        FakeDeliveryDate deliveryDate = new FakeDeliveryDate();
        deliveryDate.setDate(LocalDate.of(2020,9,13));
        LocalDate actualDate = deliveryDate.getDeliveryDate();
        assertEquals(LocalDate.of(2020,9,30),actualDate);
    }

    private class FakeDeliveryDate extends DeliveryDate {
        @Override
        protected LocalDate getNow() {
            return LocalDate.of(2020,9,13);
        }

        //setDate()メソッドの作成
        public void setDate(LocalDate date) {
        }
    }

setDate()メソッドの引数として入っているdateをフィールド変数に入れます。この時点でもテスト実行がGreenになり続けていることを確認します。

//DeliveryDateTest.java内にあるインナークラスFakeDeliveryDate
    private class FakeDeliveryDate extends DeliveryDate {
        LocalDate date;
        @Override
        protected LocalDate getNow() {
            return LocalDate.of(2020,9,13);
        }

        public void setDate(LocalDate date) {
            this.date = date; //引数をフィールド変数dateに代入する
        }
    }

フィールド変数のdateをgetNow()メソッド時に返すようにします。getNow()メソッドの処理が変わりましたが、この時点でもテスト実行がGreenになり続けていることを確認します。

//DeliveryDateTest.java内にあるインナークラスFakeDeliveryDate
    private class FakeDeliveryDate extends DeliveryDate {
        LocalDate date;
        @Override
        protected LocalDate getNow() {
            return date; //returnする値を変更する
        }

        public void setDate(LocalDate date) {
            this.date = date;
        }
    }

このように変更することで、テストケースの中で日付を設定してテスト実行することができるようになりました。

別のテストケースでもテスト実行ができるようにする

ここまで変更により、「_小の月の月末になる場合のテスト」のテストケースを日付を指定しつつ実行できるようになりました。

ここからは同様に、「_大の月の月末になる場合のテスト」のテストケースも実行できるようにします。

まずは「_大の月の月末になる場合のテスト」のコメントアウトを外します。この時点では(テスト実行日が大の月でない限り)テスト実行するとRedになります。

//DeliveryDateTest.java内にある大の月の月末テスト
    @Test
    void _大の月の月末になる場合のテスト() {
        LocalDate actualDate = new DeliveryDate().getDeliveryDate();
        assertEquals(LocalDate.of(2020,10,31),actualDate);
    }

次に、「_小の月の月末になる場合のテスト」のテストケースをコピペしてsetDate()メソッドの日付を(2020,10,13)に変更します。この時点でテスト実行してGreenになることを確認します。

//DeliveryDateTest.java内にある大の月の月末テスト
    @Test
    void _大の月の月末になる場合のテスト() {
        FakeDeliveryDate deliveryDate = new FakeDeliveryDate();
        deliveryDate.setDate(LocalDate.of(2020,10,13)); //先ほど作成したsetDate()メソッドを呼び出す
        LocalDate actualDate = deliveryDate.getDeliveryDate();
        assertEquals(LocalDate.of(2020,10,31),actualDate);
    }

これにより、「_大の月の月末になる場合のテスト」もテストができるようになりました。

おわりに:今回のレガシーコードのリファクタリングでのポイント

ここまでのリファクタリングによって、テストケースを注入することができました。

このコードはまだまだリファクタリングの余地があります。

  • 小の月と大の月に関するテストケースの拡充(実装コード上、それぞれの小の月を指定したif文で分岐していますが、テストコード上は9月しか小の月をテストできていません)
  • うるう年の場合のテストケースの拡充及びロジックの追加(本来のうるう年のルールはもっと複雑です)
  • もともとの実装コードはロングメソッドになっているので、責務を分割するためのメソッド化
  • 配送日が次月になるテストケースを拡充
  • 実装コードのロジックが間違っている箇所の修正(テストケースを拡充すると分かりますが、ロジックがそもそも間違っています)

テストケースを注入できたことで、最初よりも上記のリファクタリングも行いやすくなっているでしょう。

今回のポイントは以下の2点です。

  • まずはテストケースを1つでも作成して、とりあえずテスト実行をしてみる(頑張って、たくさんのテストを作ろうとしなくてOK)
  • テストしづらい部分を見つけて、テスト実行への障壁を排除する

また、今回に限らず、リファクタリングをする際に重要な点として、常に対象のテストを実行し続けていて、Greenでキープしたままリファクタリングを行っていることにも注目してください。

宣伝

実は本記事は、現在執筆中の技術同人誌『テストコードの注入から始めるレガシーコードのリファクタリング』の第1章の前半部分をブログ記事にしたものになります。

同人誌の中では、もっと複雑な題材も扱っていきたいと思いますので、興味がある方は下記のleanpubページからチェックをお願いします*13 。既にサンプル版は公開しています。正式版も同じURLから購入できる予定です。

leanpub.com

また、サンプル版はBoothでも公開しています。Boothでの正式版は別URLになる予定です。

nihonbuson.booth.pm

さらに、JaSST'21 Tokyoでは、今回よりも少し複雑な題材を用いて、ライブコーディング形式での発表も予定しています。

詳細は下記ページをご確認ください。

www.jasst.jp

*1:ここでは書籍『レガシーコード改善ガイド』( https://www.amazon.co.jp/dp/B01AN97W08 )の考え方を引用し、「テストコードがないコード=レガシーコード」としています

*2:本記事を作成するにあたり、TDDBC Onlineの皆さんのアドバイスが大いに役立ちました。アドバイスをいただき本当にありがとうございました!

*3:このコードを見て「なんだよ!全然複雑じゃないじゃん!現場のコードはもっと複雑だから、こんな記事を見ても意味がないや」と感じた人もいるかもしれません。しかし、本記事で伝えたいことは「複雑なコードに対処すること」ではなく、「テストコードがないものに対処すること」です。テストコードがない状態に対して、一歩でも先に進む方法を本記事では紹介します。

*4:TDDの1周目のRedで行うことと同じ手順です

*5:テスト実行した結果、成功を「Green」、失敗を「Red」と言います。本記事では以降「Green」「Red」と表記します。詳しくは書籍『テスト駆動開発』( https://www.amazon.co.jp/dp/B077D2L69C/ )を参考にしてください。

*6:Redになったとしても、悲観する必要はありません。「設定が足りないことを教えてくれた」と、テストコードを書くことによって新情報を得られたことを喜ぶべきです。

*7:JaSST'18 Tokyo招待講演「私が経験したソフトウェアテストの変遷」 www.jasst.jp

*8:TDDではリファクタリングで変更するたびにGreenになっていることを確認します。レガシーコードのリファクタリングでも同様に、変更するたびにGreenになっていることを確認します。

*9:存在しないクラスをあたかも存在するかのように記述し、コンパイルエラーの内容に従ってクラスを作成するという今回の手順は、TDDでもよく行われるやり方です。

*10:まずはコピペをすることで、論理的にはテスト失敗に変化することはないはずです

*11:今回はステップを丁寧にして、まずは実装コードをコピペして、そこからロジックを変更していっています。これにより、「呼び出しの変更に対応」→「ロジックの変更に対応」と、気にする部分を分けてリファクタリングしています。

*12:TDDサイクルのRedの時の手順と似たようなやり方です

*13:「素早く出版できる」「一度購入してもらえれば、Updateがあれば購入者に無料で提供できる」「購入者がある程度自由に価格を決められる」という点で、私が本を出版するときはLeanpubを選択しちゃいます。Paypalに慣れてない人が多いかもしれないけど。

「全網羅テスト」という言葉について 〜または、PICT活用時の落とし穴〜 #テストアドカレ

はじめに

本記事はソフトウェアテスト Advent Calendar 2020 20日目の記事です。

TL;DR

  • PICTで作成するテストケースは、特定条件下の元で網羅したテストである
  • 「PICTによって全網羅したテストを作っている」と言うのは誤解を与えるのでやめておいた方が良い
  • PICTを使う最適なタイミングが存在するので、PICTを毎回使うのではなく別の組み合わせテスト技法や単体テストの活用も検討しよう

目次

「全網羅してます」

私は「全網羅してます」という発言を聞くと気になってしまいます。もう少し細かく言うと、網羅基準がハッキリしていない状態で「全網羅しました」と言われるとツッコミを入れることがあります*1

今回は、具体的な例を元に、全網羅について考えてみます。

今回の例

Aさんは有効桁数が9桁の電卓アプリを作りました。ですが、今回の電卓アプリのテストを全くやってないことに気づきました。そこで、全網羅するような組み合わせテストを考えることにしました。

今回の組み合わせテストのやり方

Aさんは今回の組み合わせテストを

(入力数値X)(計算記号)(入力数値Y)

という3つの組み合わせで考えることにしました。

例えば、入力数値Xが1,計算記号が+,入力数値Yが2だったとすると、

1+2

となり、期待値は3となります。

PICTを使用する

この組み合わせテストを行うにあたって、AさんはPICTというツールの存在を知りました。候補の値を設定すると、勝手に組み合わせを作ってくれるらしいです。

github.com

またPICTと同様に、Excelを用いて組み合わせテストを作ってくれる、PICT Masterというツールもあるみたいです。

ja.osdn.net

PICTに設定する値

Aさんは、PICTに下記の値を候補として設定しました。

入力数値Xの候補 計算記号の候補 入力数値Yの候補
0 + 1
100 - 2
123456789 × -123456789
987654321 ÷

PICTで作成されたテストケース

上記の値を設定をして、PICTを実行することで、下記のテストケースが作成できました。

入力数値X 計算記号 入力数値Y
987654321 × 1
123456789 - 2
100 ÷ 2
0 × -123456789
987654321 ÷ -123456789
123456789 + -123456789
0 - 1
100 - -123456789
987654321 + 2
987654321 - 2
0 ÷ 2
100 + 1
123456789 ÷ 1
123456789 × 2
0 + 1
100 × 2

これらのテストケースを全て作成することで、Aさんは全網羅テストをすることができました。

めでたしめでたし…?

今回の例の検証

今回の例は本当にこれで良かったのでしょうか?少しふりかえりながら考えてみます。

作成したテストケースを考える

作成したテストケースを、期待値も含めて記載してみましょう。

入力数値X 計算記号 入力数値Y 期待値
987654321 × 1 987654321
123456789 - 2 123456787
100 ÷ 2 50
0 × -123456789 0
987654321 ÷ -123456789 -8.000000073
123456789 + -123456789 0
0 - 1 -1
100 - -123456789 123456889
987654321 + 2 987654323
987654321 - 2 987654319
0 ÷ 2 0
100 + 1 101
123456789 ÷ 1 123456789
123456789 × 2 246913578
0 + 1 1
100 × 2 200

気になる内容

勘の良い人は気付いたかもしれません。実はAさんが今回作成した組み合わせテストでは、気にしたい下記の2つの内容がテストケースに含まれていません。

  • 桁数オーバーの例(999999999+1など)
  • ゼロ除算の例(1÷0など)

どうしてこの2つの内容が含まれていないのでしょう?

それは、「PICTを用いて全網羅をした」と考えていたことに対して、2つの落とし穴にハマっているからです。

全網羅の落とし穴その1:網羅基準

(デフォルトの設定で)PICTで作成されるテストケースは、全組み合わせの網羅ではなく、二因子間網羅です。

因子とは、テストの組み合わせを考えるときの項目名をいいます。今回の場合は「入力数値X」「計算記号」「入力数値Y」です。

一方、1つの因子に対しての具体的な値を水準と言います。今回の場合、因子が「入力数値X」の水準は「0」「100」「123456789」「987654321」となります。

例えば、今回の例の場合、入力数値X、計算記号、入力数値Yの3つの因子を組み合わせたパターンは下記の通りになります。

入力数値X 計算記号 入力数値Y 期待値
0 + 1 1
0 + 2 2
0 + -123456789 -123456789
0 - 1 -1
0 - 2 -2
0 - -123456789 123456789
0 × 1 0
0 × 2 0
0 × -123456789 0
0 ÷ 1 0
0 ÷ 2 0
0 ÷ -123456789 0
100 + 1 101
100 + 2 102
100 + -123456789 -123456689
100 - 1 99
100 - 2 98
100 - -123456789 123456889
100 × 1 100
100 × 2 200
100 × -123456789 桁数エラー
100 ÷ 1 100
100 ÷ 2 50
100 ÷ -123456789 -8.1E-07
123456789 + 1 -8.10000007
123456789 + 2 123456791
123456789 + -123456789 0
123456789 - 1 123456788
123456789 - 2 123456787
123456789 - -123456789 246913578
123456789 × 1 123456789
123456789 × 2 246913578
123456789 × -123456789 桁数エラー
123456789 ÷ 1 123456789
123456789 ÷ 2 61728394.5
123456789 ÷ -123456789 -1
987654321 + 1 987654322
987654321 + 2 987654323
987654321 + -123456789 864197532
987654321 - 1 987654320
987654321 - 2 987654319
987654321 - -123456789 1111111110
987654321 × 1 987654321
987654321 × 2 1975308642
987654321 × -123456789 桁数エラー
987654321 ÷ 1 987654321
987654321 ÷ 2 493827160.5
987654321 ÷ -123456789 -8.000000073

このように、3つを組み合わせたパターンの網羅(三因子間網羅)をすると、桁数エラーの組み合わせがあることが分かります。

一方で、PICTが(デフォルトの設定で)行うのは二因子間網羅です*2

二因子間網羅とは、2つの因子の間であれば全ての水準を網羅していることを示しています。

例えば、今回PICTで作成したテストケースのうち、入力数値Xが0であるテストケースを抜き出してみます。

入力数値X 計算記号 入力数値Y 期待値
0 × -123456789 0
0 - 1 -1
0 ÷ 2 0
0 + 1 1

まず、計算記号に着目すると、「+」「-」「×」「÷」の全ての水準が登場していることが分かります(青字部分)。つまり、因子「入力数値X」の水準「0」に対して、因子「計算記号」の水準全てと組み合わせていることになります。

次に、入力数値Yに着目すると、入力数値Yに設定することにしていた「1」「2」「-123456789」の全ての水準が登場していることが分かります(緑字部分)。つまり、因子「入力数値X」の水準「0」に対して、因子「入力数値Y」の水準全てと組み合わせていることになります。

他の入力数値Xで設定している「100」「123456789」「987654321」も同様に調査すると、計算記号と入力数値Yで設定している値それぞれが少なくとも1回以上登場していることが分かります。

入力数値が「100」の場合

入力数値X 計算記号 入力数値Y 期待値
100 ÷ 2 50
100 - -123456789 123456889
100 + 1 101
100 × 2 200

入力数値が「123456789」の場合

入力数値X 計算記号 入力数値Y 期待値
123456789 - 2 123456787
123456789 + -123456789 0
123456789 ÷ 1 123456789
123456789 × 2 246913578

入力数値が「987654321」の場合

入力数値X 計算記号 入力数値Y 期待値
987654321 × 1 987654321
987654321 ÷ -123456789 -8.000000073
987654321 + 2 987654323
987654321 - 2 987654319

つまり、因子「入力数値X」因子「計算記号」の網羅や、因子「入力数値X」因子「入力数値Y」の網羅ができているということです。

このように、PICTで作成される二因子間網羅では、2つの因子の間では全ての水準が登場していることになります。

ただし、下記の例のように、三因子の組み合わせによって初めて分かるようなテストが、二因子間網羅のテストケースに入る保証はありません*3

入力数値X 計算記号 入力数値Y 期待値
100 × -123456789 桁数エラー
123456789 × -123456789 桁数エラー
987654321 × -123456789 桁数エラー

つまり、「PICTを使って作成されたテストを全網羅したテストケース」が、すなわち優れたテストケースとは限りません。

全網羅の落とし穴その2:設定値

PICTでは、自分が設定した因子や水準を元にテストケースが作成されます。つまり、作成されるテストケースは自分が設定した因子や水準に依存しています。

今回の場合、因子「入力数値Y」に水準「0」がありません。この状態でPICTでテストケースを作成しても、絶対に「(入力数値X)÷0」が発生しません。

このように、自分が設定した因子や水準が適切でないと、二因子間網羅や三因子間網羅をしても確認したいテストが作れなくなります*4

落とし穴にハマらないために:PICTの使うタイミングについて

「組み合わせテストならばPICT(PICT Master)を使えば良い」というイメージが付きがちですが、PICTを使うタイミングはきちんと考えた方が良いです。

すぐにPICTを用いるのではなく、下記2点に注目しましょう。

  • そのテストは本当に組み合わせて考えるべきなのか
  • その組み合わせは無則な組み合わせになっているのか

注目点1:そのテストは本当に組み合わせて考えるべきなのか

今回の例はそもそも組み合わせテストが必要なのでしょうか。

例えば、「ゼロ除算や桁数エラーは組み合わせテストで行うのではなく、単体のテストで行うべき」という意見があるでしょう。私もこの意見に賛成です。*5

例えば、「1÷0」のようなテストは単体テストで行うことで、組み合わせテストではゼロ除算のテストが無くても良いでしょう。

なお、「ゼロ除算のテストはテストコードで行なっているから、画面単体のテストは実施しない」という考え方については少し反対です。

テストコードで行なっているゼロ除算のテストは「ゼロ除算の返り値(Exception)が返ってくる」ことを確認しているはずです。

それとは別に「ゼロ除算の返り値(Exception)が来た場合には、その旨を画面に表示する」というテストを行うべきでしょう。ただし、これを組み合わせテストで行う必要はない(単体テストで実施すれば良い)と思っています*6

注目点2:その組み合わせは無則な組み合わせになっているのか

PICTは無則な組み合わせテストの時に使う方が良いです。

無則な組み合わせというのは、因子間でのどの組み合わせによっても期待値に影響がないものを指します。

一方、因子間の組み合わせによって期待値に影響があるものを有則な組み合わせと言います。

例えば今回の場合、因子「計算記号」の水準「÷」と因子「入力数値Y」の水準「0」を組み合わせると、エラー(ゼロ除算)が発生するので、有則な組み合わせと言えます。

無則な組み合わせの例としては、因子「入力数値」と因子「画面の明るさ調整」などです。

このような無則な組み合わせの時に初めてPICTを検討しましょう*7*8

有則な組み合わせのテストはどのようにすれば良いのか

それでは、今回のように有則な組み合わせの場合にはどのようにテストを考えれば良いのでしょうか?

PICTを活用した二因子間網羅ではなく、デシジョンテーブル・CFD・原因結果グラフといったテスト技法の活用を検討すると良いでしょう。

これらの技法の説明については、WACATEの過去資料に素晴らしいスライドがあったので、そちらを参考にしてください。*9

デシジョンテーブル

www2.slideshare.net

CFD

www2.slideshare.net

原因結果グラフ

www2.slideshare.net

デシジョンテーブルを活用する際に、全ての因子の組み合わせだと組み合わせ爆発が起きてしまう。…ということについては、nemorineさんの記事などを参考にしてください。

nemorine.hateblo.jp

おわりに:PICTで作成した全網羅なテストとは

結局、PICTで作成したテストケースは全網羅をしていないのでしょうか?いいえ、ある条件下の元で全網羅しているテストです。

PICTで今回作成したテストケースというのは、「自ら設定した因子と水準」「二因子間網羅という網羅基準」において全網羅しているテストケースといえます。*10

この考えを抜きにして、「PICTによって全網羅したテストを作っています」と言うのは誤解を与えるのでやめておいた方が良いでしょう。

PICTは適切なタイミングで使うことで、効果的にテストケースを削減できます。落とし穴にハマらないようにしつつPICTを使っていきましょう!

*1:思えば昨年は「機能テスト」という言葉が気になって記事を書いてましたね…。nihonbuson.hatenadiary.jp

*2:PICT実行時にオプションを指定することで、三因子間網羅のテストを作成することは可能です。しかし、当然ですが二因子間網羅の場合よりもテストケース数は格段に増えます。

*3:絶対に入らない訳ではなく、入る可能性もあります

*4:もしも、私がAさんの作業内容を見たら、「どうしてPICTを使って組み合わせテストを行おうと考えたのか」「どうして入力数値Xの水準が『0』『100』『123456789』『987654321』で入力数値Yの水準が『1』『2』『-123456789』なのか」について質問し、「(もしもこのまま組み合わせテストを行う場合)『0』や『999999999』を入力数値Yの水準に加える」ことを提案することになるでしょう。

*5:今回は、前提として

今回の電卓アプリのテストを全くやってないことに気づきました。

と書いているため、ゼロ除算のテストがないまま組み合わせテストを終わらせてリリースを迎えるのはどうかと思い、組み合わせテストの中身について指摘しています。本来ならば、「そもそも組み合わせテストではない方が良いのでは?」と提案します。

*6:テストコードではゼロ除算のテストを境界値分析を用いてテストしている場合は、画面表示のテストに関しては境界値のテストを行わず、同値分割でのテストケースにするかもしれません。

*7:そもそも、因子「入力数値」と因子「画面の明るさ調整」で組み合わせのテストをやるべきなのかについては検討する必要があるでしょう。

*8:無則な組み合わせテストを活用している技法の1つにHAYST法(HAYST法ツール)があります。HAYST法では、一見すると関係なさそうな因子水準を組み合わせることで、思いもよらないバグが見つかることを狙いにしています。なのでHAYST法を使う前提として、ある程度のテストを行なっており、それでもバグが見つからない時に最後に行うものだという理解です。ほとんどテストしてない時点で有則な因子同士を用いてHAYST法を使うことは、本来の使い方とは違うのかなと思っています。(それによってバグは見つかるかもしれないが、本来のHAYST法の狙いではないという認識)

*9:「(調べたいテスト技法) WACATE」と入力してググることで、良質な資料を見つけることができるという知見を得ました。

*10:ここまで書いて、「全網羅」という言葉には、「全ての組み合わせを網羅している」という意味で使う場合と、「特定条件下の元での組み合わせパターンを全て網羅している」という意味で使う場合の2つあることに気付きました。「特定条件下の元で」を意識しないと、この2つの意味合いの違いが無い状態で言葉を使うことになりそうです。

【翻訳記事】TDD: 目的と実践

目次

はじめに

今回は著者本人の許可をもらった上で、TDD: 目的と実践(原題は「TDD: Purposes and Practices」)を翻訳したので紹介します。

www.industriallogic.com

この記事はIndustrial Logic社のTim Ottingerが書いた記事です。Tim OttingerはClean Codeの執筆にも関わっています。

また、Industrial Logic社はAgileなどに関するコーチングやトレーニングを行なっている会社です。そのため、記事の中ではIndustrial Logic社のe-learningのコースの多くがリンクされていますが、それらの宣伝を抜きにしても、非常に良い記事だと思います。

これ以降は元記事を翻訳したものになります。


TDD: 目的と実践

テスト駆動開発(TDD)は、無駄な争い、遅延、動揺を招く方法と誤解されることがよくあります。

誤解や不正確な説明は非常に苦痛であり、開発者はフラストレーションを溜めて、時には実践全体が有害で無意味であり、「死んだ」と宣言することもありました。

f:id:nihonbuson:20201109091043p:plain
悪いTDDは死んだ。

おそらく、人々がこの重要な実践をより健康的で生産的な形で理解してもらうことができるでしょう。

TDDとは何ですか?

機械的には、TDDは非常に単純です。

f:id:nihonbuson:20201109092012p:plain
下の文章で説明したTDDの4段階の図

  1. テストで期待される動作をコードで実装していないために失敗する(マイクロ)テストを記述します。
  2. テストが成功するコードをすぐに記述します。図で示した赤​​い矢印に注意してください。失敗したテストがある場合、たとえ多くの変更を元に戻すことを意味しても、プログラマーの唯一の仕事はすべてのテストに成功することです。
  3. コードがこれまでの意図した機能を満たしていることを証明することで大胆になります。それによって、新しい機能が明確、意図的、明白な方法でコードに適切になるように、コードとテストを修正します。
  4. コードがきめ細かく動作し、すべてのテストが成功したので、コードをソース管理システムにコミットし、他の開発者が行った変更を取得します。すべてのテストはまだ成功しています。

TDDは単純な4ステップのプロセスであるため、ほとんどの人は必要なスキルとテクニックを過小評価していました。

  • リファクタリングをサポートするテストを書くことは、いくつかのテクニックを含むスキルです。
  • テストが成功するために必要なコードのみを書くには、かなりの自制心が必要です。
  • リファクタリングでは、コードの構造を変えながら常にテストが成功し続けるために、多くの技術スキルとコード構造のある程度の認識が必要です。
  • 他の開発者と継続的に統一するには、一種の社会契約が必要です。私たちは皆、他の人のコードがうまく書かれていて、すべてのテストが成功していることを当てにしています。

TDDの意図

TDDはプログラミングを衛生的に保つ仕組みです。継続的インテグレーションによるインクリメンタルでイテレーティブな開発をサポートしながら、コードを整然と保ちます。

ここでの整然としたという言葉は、乱雑さ、混乱、ちらかっていることがないことを示唆しています。

整然としたコードは、コードの臭い(Code smells)がないと表現でき、完全に間違っているわけではないでしょう。

コードの臭いとその改善方法を学ぶことで、大きな進歩を遂げることができます。私たちのCode smells albumと無料のcode-smells-to-refactoringsのチートシートは、あなたを良い方向に導くのに役立ちます。

コードの臭いだけに焦点を当てても、プログラマーが整然としたコードを書くことを理解することはできません。他にも必要なスキルがあります。

整然としたコードは、理解と修正が容易なコード(柔軟であり、さらなる開発を妨げないコード)として説明することを好むかもしれません。

今後のブログ記事では、良いコード編成とソースコードの信号雑音比(signal-to-noise ratio、SN比)に関するアイデアの組み合わせについて議論します。ここでは他にも取り上げたいことがあります。

インクリメンタル開発とは、定義されたすべての機能を完成させ、1つのパスまたはセッションですべての制約を収めようとするのではなく、一度に少しずつコードに機能や動作を追加することです。

機能を追加するたびに、チームにインクリメンタル開発を依頼します。アジャイルチームでは、できるだけ頻繁に新たなものを出すように計画しています。

イテレーティブ開発とは、既存のコードを見直し、必要に応じて手直したり、手を加えたりして、全体の設計を改善することです。

これらの再検討は、やり直すといった無駄ではなく、むしろ衛生的な無駄の削減です。私たちは学んだことを取り入れて適用し、すべての機能を考慮してコードが「最初から何をしていたかを分かっているように見える (Ward Cunningham氏の言葉を引用)」ようにします。これは、最初のバージョンが書かれて以来、追加または複雑化したもの、ドメイン知識を得られたものです。

継続的インテグレーションに任せることで、ソフトウェアの変更は、できるだけ早く(可能であれば1日に何回も)他の開発者と共有されます。

他の開発者とコードを共有するには、1日に何度も「安全」かつ「完成」(「完成」の値が小さい場合)である必要があります。そのためには、よりインクリメンタルなアプローチと、高速なテストによって提供される安全性が必要です。

自信を持って頻繁に見直し、修正、機能の追加をして、コードを共有することを安全に行えるような実践(作業衛生)がない限り、これらのふるまいはすべて危険になるでしょう。

これがTDDを行う理由です。TDDはこれらの開発者の行動に安全性をもたらします。

TDDはTestingですか?

一部の人々は、TDDはテスターが行うTesting手法だと思っている人もいます。

TDDは、ソフトウェア開発者がリファクタリング継続的インテグレーションを成功させるために使用する、ソフトウェア開発作業を衛生的に保つ仕組みです。

例に基づいたテスト分野がいくつかありますが、これらはすべて、意図したとおりに使われている良いものです。ATDDとBDDは、テスターを要件と設計の実践にシフトレフトして実行するのが最適かもしれません。リファクタリングをサポートする例に基づいた実践は、マイクロテストユニットテスト、場合によってはストーリーテストなど、最も詳細になる傾向があります。

純化しすぎではありますが、Testingは、製品の使用に対する適合性を評価または確認しようとする活動であると考えてください。それがTestingの場合、TDDはTestingではありません。

TDDは、システムが動いていること、または機能が明確に満たされていることを証明しません。TDDは、主にリファクタリングの条件を作るために存在します。そのためにテスト(マイクロテスト)を使っているからといって、それがTestingの実践になるわけではありません。

TDDの目標は、迅速なリファクタリングの環境を作り出すことであり、より高レベルのテストのほとんどは、実行が遅すぎてこの目的には役立ちません。

TDDサイクルは高速です。1時間に少なくとも12回サイクルを完了できなければ、TDDサイクルを使用する余裕がなくなるまで、作業の速度が低下することになります。私たちはそれを素早く行うか、(最終的には)まったくやらないかのどちらかになります。

UIレベルのテストは良いのですが、UIレベルのテストを実行するためには、アプリケーションのインスタンスとそのすべてのサポートサービスを立ち上げる必要があり、これらのテストは画面またはページの更新を待たなければなりません。これには長い時間がかかり、開発者が生産性を維持しながら1時間に6〜12回実行することはできません。

ストーリーテストやシステムテストはあまりにも多くのセットアップやサポートが必要になるため、2分またはコードの2,3行に1回実行できないという考えがあります。

つまり、TDDはプログラムが書かれた後にテスターができるものではなく、テスターの目的には適していないということです。しかし、テスターが発見する欠陥の数を減らすことができ、テスターがより大きな問題に集中することができます。

TDDはユニットテストを書くことですか?

TDDの目的はユニットテストを作ることであり、もちろん、ユニットテストの密度はコードカバレッジのパーセンテージで測定できると考える人もいます。これは誤りです。

ユニットテストを作成し、ソースコードのテストカバレッジを増やす方法はたくさんあり、そのうちの1つがTDDというだけです。

確かに、少しのコードを書いて、そのコードを通るすべてのパスをカバーするユニットテストを書くことによって、非常に良いテストカバレッジを得ることができ、多くの素晴らしいユニットテストを生成することができます。これらのテストは、コードが作成された後に書くことができます。なぜ違うのでしょうか?

実際、コードの完成後にテストを作成する(コードの検証に必要な一連のテストを正確に作成する)方が効率的かもしれません。TDD経由でコードを完成させ、TDDでは考慮しなかった状況をカバーするテストを追加していることが分かります。

ただし、TDDの目的は、ユニットテストを作成してテストカバレッジを増やすことではありません。これは起こりますが、それは付随的な作用でもあります。

ユニットテストを行うことは良いことであり(さらに高レベルのテストは素晴らしいことです)、チームから回避された欠陥を減らすことができますが、ここでの本当の目標は、コードをリファクタリングするための環境をすばやく作成することです。

TDDだけで良いのでしょうか?

システムが動くことを証明するには不十分であるため、TDDは役に立たないと主張する人もいます。TDDは小さな部品が確実に動くようにするには役立つようですが、組み立てられたシステムが正しく動作することを証明するには、ほとんど(またはまったく)役に立ちません。

彼らの言うことは正しいです。それがTDDの目的ではありません。

しかし、この論理を使えば、抗生物質線維筋痛症を治さないため、役に立たないと言うこともできます。同様に、ツーリング用の自動車も、道路があるところにしか乗れないので、無価値になります。

TDDがソフトウェア品質に対して完全に十分ではないと主張することは、ポイントを外しています。重要なのは、TDDは(うまくやれば)目的には容易に十分になりますが、その目的はシステムの十分性や正確性を証明することではないということです。

システムが正常に動くことを確認したい場合は、Testingが必要です。目的や原動力が異なるため、TDDとは異なります。

上記のTDDはTestingではないという議論を参照してください。

TDDは良い設計を強要しません

はい。TDDでアサーションが進むことについては納得しますが、優れた設計を強要することはTDDの目的ではありません。

TDDでうまく動けば、迅速なフィードバックで設計を修正できます。イテレーティブな作業およびインクリメンタルな作業は簡単に可能であり、粗悪な設計も短期間ではるかに優れた設計にリファクタリングできます。

TDDでは設計を行いませんが、設計を改善するための多くの機会を提供します。

実際、TDDサイクルのすべてのループには、リファクタリングのための時間が明示的に提供されています。その時間を利用しないと、リファクタリングが必要なのにリファクタリングされていないコードになってしまう可能性が高いのです。

TDDループを使い始めたからといって自動的にリファクタリングの使い手になるわけではありませんが、TDDプロセスは、これらのスキルを学ぶ機会を提供します(Refactoring albumでも説明しています)。

悪いテストはリファクタリングを妨げます

繰り返しますが、ここは完全に合意します。

悪いテストがあることは、テストがないことよりも悪い場合があります。

TDDサイクルを無意識にループすることで、すべてのテストとすべてのコードが完璧になるとしたら、それは素晴らしいです。私は初心者の前にTDDの図を投げて、立ち去ることができたら嬉しいでしょう。

しかし、TDDは一連のスキルと態度です。これらのスキルと態度は、実践と情報を通じて開発されなければなりません。

TDDを行う私たちのほとんどは、これらのスキルとテクニックをしばらくの間、勉強しなければなりませんでした。これらは明白で簡単なものではありません。TDDをどのように行うべきかについては、さまざまな陣営に分かれています。

我々はTDDにおける、いわゆる「アンチパターン」と失敗の方法を文書化しました。

ただし、TDDサイクルの中にはリファクタリングフェーズが含まれています。リファクタリングフェーズでは、テストと設計をリファクタリングできるため、長いセットアップ手順と脆弱なタイミングでの酷いテストに耐える必要がなくなります。

また、TDDは「テストを持つ」ことではなく、コードの変更や追加を迅速かつ簡単にするために必要なことを行うことを目的としています。

テストによって気分を害したら、切り捨てましょう。テストは、小さく、安価で、高速で、簡単に破棄できる必要があります。マイクロテストの書き方を学ぶことは、学ぶ必要のあるテクニックの1つです。これは、Microtesting albumで教えているテクニックです。

Twitter上のある批評家は、TDDはモックとフェイクに依存しているため、テストを数十回(場合によっては数百回)書き直さないとコードを安全にリファクタリングできないと指摘しています。もしそうであれば、TDDが必要とする方法でリファクタリングをサポートするために、フェイクやモックを使う手法を学ぶことをお勧めします。この手法は、さまざまなソースから学ぶことができます。もちろん、Faking and Mocking albumで教えています。

なぜ悪評が多いのですか?

悪評は主に正当に苦労した人々からのものです。彼らはブログやソーシャルメディアを利用して、どのように苦労したか、そしてなぜ彼らが気になる困難なことをやめたのかを説明します。彼らは他の人に警告することで、イライラする無駄な時間から救います。

これは私が同意する行動です。私は、酷いTDDをしたり、意図しない理由でTDDをするよりも、TDDをしない方がいいと思います。ひどいTDDをして、それが提供しない利益を期待しますか?酷そうですね。

TDDを上手に行うことを学ぶための素晴らしいリソースはたくさんあります。多くのブログ記事があります。Kent Beck, Jeff Langr, James Grenningなどの専門家によって書かれた、TDDを教える素晴らしい本がいくつかあります。また、youtubeやオンライントレーニング会社のビデオもたくさんあります。

さらに、Industrial Logicでは、レガシーコードを扱うためのヘルプを含む「The Testing And Refactoring Box Set」と呼ばれるボックスセットに、(上記のアルバムを含む)最高の学習の多くを収集しました。また、これらのリソースは、ライブのTesting and Refactoring Workshopsでも活用しています。

TDDがうまくいけば、リファクタリングをサポートし、継続的インテグレーションによるインクリメンタルかつイテレーティブな開発を可能にします。あなたがしていることがこれらの働き方を可能にできず、サポートしない場合は、自身やあなたのバージョンのプロセスを非難するのではなく、それを動かすテクニックを勉強することを検討してください。

TDDは作業衛生です。それは学ばなければならない多くのテクニックを含む包括的な練習です。それをうまくやることに投資すれば、配当を受けることができます。

初期のレビュアーであるJesusVega、Josh Kerievsky、Mike Rieser、Jeff Langr、John Borys、JennyTarwaterに特に感謝します。

今年もJaSST Reviewを開催します! #jasstreview

一昨年から始まりましたJaSST Reviewを今年も開催します。3回目の開催です!

本記事では今回の発表内容をざっと紹介していきます。読んだ上で興味がある発表がありましたら、ぜひイベントに参加登録をお願いします!

www.jasst.jp

目次

JaSST Reviewとは何か

JaSSTとはソフトウェアテストシンポジウム(JaSST)のことで、2003年から全国各地で開催されているテストのイベントです。全国で年間10回以上開催されています。

そしてJaSST ReviewはJaSSTの一つで、ソフトウェアレビューに特化したイベントです。

詳しくは以前書いた下記記事を参照してください。

nihonbuson.hatenadiary.jp

今回のJaSST Reviewのテーマ

今回のテーマは「レビュー対象へのアプローチ方法」です。

以下、JaSST Review'20のページから抜粋。

   「多くの気になることのうち、なぜその内容を指摘しようと考えたのか?」

   「気になることって、そもそもどのようにして出てくるのか?」

   「レビュー対象の中から、作成者に最初に質問した部分はどのように選択されているのか?」

このような質問をレビュアーにしたとしても、「なんとなく」と答えてしまう方が多いかもしれません。

一方で、明確な言語化はしていないものの、自分自身の頭の中では決まった方針があって、それをもとにレビュー対象にアプローチしているレビュアーもいると考えています。

このような、「レビュー対象へのアプローチ方法」「レビュー対象を探る際の狙い」について、今回は議論したいと思っています。

自分たちがとったレビューでの言動がどんな意図を持っているのか、改めて考えられるようなコンテンツになっていると思います。

以降、どのようなコンテンツがあるのか紹介していきます。(主に予稿集の原稿を読んだ感想)

講演1 : 専門書が出版されるまでの編集者の思考と行動 ~編集者はどのように校正・校閲しているか~

1つ目の講演は日科技連出版社の鈴木兄宏さんによる講演です。

鈴木さんは、JaSST Review実行委員の安達さんが書かれた『ソフトウェアプロセス改善手法SaPID入門』などの書籍の編集を担当した方です。

発表当日は、当時の安達さんとのレビューのやり取りも紹介してくれる予定です。

安達さんによる講演の紹介はこちら。

予稿集では「講演当日までお楽しみ」という内容が多くありますが、そのスライドページのタイトルを見ただけでもワクワクするものがいっぱいあります。

また、鈴木さんは「狩野モデル」で有名な狩野紀昭先生の下で論文を書かれています。その当時の狩野先生の言葉なども発表の中で出てくる予定です。

書籍出版という他業種の内容とはいえ、文章に拘りを持つ分野の具体的な例を持ったレビューのやり取りは、参考になるのではないでしょうか?

講演2 : ソフトウェア設計における意思決定とそのレビューの秘訣

2つ目の講演はウルフチーフの川島義隆さんによる講演です。

川島さんは、現在個人事業主として株式会社ウルフチーフを設立し、ソフトウェアアーキテクチャ設計に関する支援を行なっている方です。

また、JJUG等でも多く登壇されています。

有名なスライドでいうと、リクルートさんで行なっていた新卒研修の資料「現代的システム開発概論」やJJUG CCC 2018 Fallでの発表資料「思考停止しないアーキテクチャ設計」などがあります。

speakerdeck.com

www.slideshare.net

今回の発表では、垂直思考、水平思考といった人間の思考の話や、レビュー時によく出てくる事象とその対策について話していただきます。

川島さん本人からもこんな予告が。

以下のページの内容も事前に読んでおくと、より当日が楽しめるかもしれません。

scrapbox.io

事例紹介1 : 刺激語カードを用いたソフトウェアレビューの実践について ~アイデアを刺激し意識外から観点を得る

今回から新たな取り組みとして事例募集をしました。(本ブログでも、事例募集に関する記事を書きました

結果、多数の応募の中から2名の方に発表していただくことになりました。

1人目の発表は、エムスリーの中塚裕美子さんによる講演です。

今回の発表では「刺激語カード」を用いて、観点を得ていくやり方での事例を紹介していただきます。

刺激語カード自体は、レビューやソフトウェアテスト以外の分野から持ってきたアイデアのようですので、どのようにレビューに応用して使っているのかなども発表されるのかなと期待しています。

また、この発表については既に紹介記事がありますので、そちらも参考にしてください。

www.m3tech.blog

事例紹介2 : レビューイの力を引き出すフィードバックのチューニング~チーム外からの支援で見えてきた、成熟度に応じた「問いかけ」の調整方法と各種レビューへの適用可能性

2人目の発表は、グロース・アーキテクチャ&チームスの常盤香央里さんによる講演です。

今回の発表では、チームの成熟度によって問いかけの方法を変えていくという事例を紹介していただきます。

レビューの時はもちろん、普段のチーム内の会話でも活かすことができる、問いかけ方法の考え方かなと思いました。

常盤さん本人による講演の紹介はこちら。

事例紹介のお二方は事例投稿募集なので、講演内容の依頼まではしていませんでしたが、今回のJaSST Reviewでのテーマである「レビュー対象へのアプローチ方法」に沿った内容になっているようで大変嬉しいです!

おわりに

今回のJaSST Reviewでは以上の4名の方の発表をお届けします。

またイベントの最後では、鈴木さん、川島さん、私の3人がパネリストになってパネルディスカッションをすることで、さらにレビューについて深掘りしていきたいと思います。

まだまだ参加登録募集中です。気になる方がいましたら、参加申し込みをお願いします!

www.jasst.jp

#RSGT2021 では開発にテスト活動を浸透させる話を発表したい!

現在の会社に転職してから、QAと開発の在り方について今まで以上に考えるようになりました。

また、今年に入ってからは、その考えを社外でも発表するようになりました。

2月にあったDevelopers Summit 2020では、「テストコードを書き始める前に考えるべきテストの話」と題して発表しました。この発表では、開発者自身がどのようにテストを考えていくべきかの基礎的な部分をお伝えしました。

結果、たくさんの反響をいただき、また何社の方から講演のお声がけもいただきました。

6月にあったScrum Fest Osaka 2020では「Agile Testingのエッセンス」と題して発表しました。この発表では、開発者とQAがどのようにコラボしていくのか、事例を含めてお伝えしました。

発表後には、複数の現場から「実例マッピングを使ってるよ!」という声をいただくようになりました。

今月あったD3QAでは「テスト活動の納得感を持ってテストケースを激減させた話」と題して発表しました。この発表では、QAがテスト技術を用いつつ、どのようにして開発チームに近づいていったかという話をしました。

結果、配信していたYoutube liveでは数百人の方に視聴していただき、たくさんのポジティブなフィードバックもいただきました。

そして、ここまでの発表を踏まえて、私は来年1月にあるRSGT2021のプロポーザルを提出することにしました。

発表タイトルは「Scrumチームに「テストは活動だ」という意識を浸透させるまでの物語」です。

ここまでの発表では、開発者自身のテストの話、開発者とQAのコラボの話、QAチームの話をしてきましたが、そのどれとも違う視点での話になります。

「テスト=TDD」というような認識を伝えたいのでも、テストを作業としてこなすやり方を伝えたいのでもありません。

創造的な活動としてテストを考えることで、開発実装前から、設計よりも前からテストの活動を行うことができます。それにより、以前よりも品質の良い&コストのかからない製品を作り出すことができます。

今回はそのようなやり方を紹介するだけでなく、どのようにその考え方を開発者に染み込ませていったか話す予定です。(ちなみに、「伝えた」のではなく「染み込ませた」のがポイントです)

この発表が気になる方は、下記プロポーザルのページへ行き、タイトルの下にあるハートボタンを押していただけると嬉しいです!

confengine.com

9月末までの投票となっておりますが、よろしくお願いします!

レビュー時のアプローチを考えてみませんか? #jasstreview

今年もJaSST Reviewを開催します!

jasst.jp

今年のテーマは「レビュー対象へのアプローチ方法」です。

この「アプローチ」についてもう少し考えてみましょう。

アプローチとは何か?

weblio国語辞典には以下のように書かれています。

①ある目的のために人に近づくこと。親しくなろうとすること。

②学問・研究などの、対象に接近すること。また、接近のしかた。研究法。 「歴史的な観点から-する」

レビューについて言えば、「どのように問題点に近づいていくか」を考えることかなと思っています。「問題点に対してすぐに指摘できる」ではありません。

歯医者を例に考える

歯医者さんとのやり取りは、問題へのアプローチそのものだと思っています。

歯が痛くて歯科医院に行った時、歯科医院に着いてすぐに歯を抜くことはありません。

以下のようなプロセスを踏むと思います。

  1. 患者に現在どこが痛むのか聞く
  2. 全体的に歯を見て、どこに虫歯があるのか見つける
  3. 今後の方針を患者と相談して、治療する順番を決める(複数の虫歯があった場合)
  4. 歯のレントゲンを取り、虫歯が神経まで届いているのか確認する
  5. 患者の反応を見ながら虫歯の治療を行う

f:id:nihonbuson:20200722190236p:plain

これにより、32本の歯から徐々に対象の歯を絞っていき、虫歯の状態をだんだん明確にしています。

その際には患者とのコミュニケーションを取ることで、治療の優先順位も決定しています。

これはまさに問題(虫歯)に対して近づいていく(アプローチする)行動そのものです。

このアプローチでは、現在の歯のレントゲンというデータだけでなく、どの部分で患者が痛みを感じているのかなどの反応を見つつ、麻酔が必要かなどを判断していることが分かります。

レビューの場合に当てはめて考える

レビューの場合でも、たくさんのレビュー対象から問題がありそうな部分を絞っていき、問題点をだんだん明確にします。

もちろん、レビュー対象物だけでなく、レビュー対象物の作成者とコミュニケーションを取ることで、解決すべき問題点を見つけたり、優先的に伝えるべき内容を探っていきます。

ただし、歯科医院の場合と違い、どのようなアプローチで問題点を見つけるのか、レビュー対象物の作成者とどのような会話をするのかなどは定かではありません。

このJaSST Reviewというシンポジウムを通じて、レビュー対象へのアプローチ方法について考えていきたいと思っています。

JaSST Reviewでは事例募集をしています!

この「レビュー対象へのアプローチ方法」という話は、まだまだ未開拓の地だと感じています。その一方で、多くの現場ではレビューを行なっていると思います。

そこで、皆さんの現場で行なっている、レビュー対象へのアプローチ方法について事例募集をすることにしました!

締め切りは今月末となります。

参加者の方々と共有して、レビューについて改めて考えてみませんか?

「そんなに新しいことやってないし…」と思っている人へ

大丈夫です。この部分は未開拓の地なので、事例発表をする人なりの言葉で語ってもらえれば、それは他の人にも発見が多いと考えています。

つまり、すでに色々な考え方や技法があるテストや開発の分野に比べて、すぐに新規性のある発表になります!

「事例発表をしたいけど、まだ発表内容が固まってない…」という人へ

まずは「事例発表したいと思っている」と言ってもらえる(ツイート、DM、直接口頭でこっそり教える)と助かります!*1

また、何回も応募して内容をどんどんブラッシュアップして頂いても構いません。

皆様からのご応募をお待ちしております!

*1:運営は「何人応募してくれるのかな…?」と不安でいっぱいです

「テスト駆動開発の『駆動』は誤訳なんじゃないか」と言われて改めて考えた話

先日、社内のSlackでこんなことを言われました。

f:id:nihonbuson:20200628162510p:plain

TDDとかのdrivenを駆動って訳すの誤訳じゃないのかと思うんですけど、どう思いますか?

意味合いは駆動より操縦とか運転なんだと思うんですが

そこで「駆動」の意味を改めて考えてみました。

辞書で調べてみる

goo辞書では以下のように書かれています。

[名](スル)動力を伝えて動かすこと。「四輪駆動」「駆動輪」

書籍から考える

書籍『エクストリームプログラミング』の第2章には以下のように書かれています。

「運転というのはね、車を正しい方向に走らせることじゃないの。常に注意を払って、こっちに行ったら少し戻して、あっちに行ったら少し戻して、そうやって軌道修正していくものよ」

これがXPのパラダイムだ。注意して、適応して、変更する。

なぜ「駆動」が誤訳だと感じてしまったのか

テスト駆動開発(TDD)は「test-driven development」の略であるように、直訳すると「運転」が当てはまる気がします。

書籍『エクストリームプログラミング』の話にあるように、ここでの「driven」は微調整を繰り返すことこそが重要に感じられます。*1

一方で「駆動」だと、「動かす」というニュアンスが強くなり、「微調整を繰り返す」という意味合いが薄くなっているように思えます。

もしかしてリファクタリングが蔑ろにされる理由の1つ?

さらに最近では「テスト駆動開発=テストをきっかけとして開発を行う」という認識の人も出てきているように感じます。*2

昨年のSelenium Confでのt_wadaさんの講演では、リファクタリングが行われなくなってくる理由について、次のように語っていました。(t-wadaのブログより引用)

黄金の回転を構成するひとつひとつの矢印が、単一の目的を持ち、十分な長さを持ち、 きちんとした軌跡を描いて回転している限りは、テスト駆動開発は大きなパワーを発揮し続けます。

しかしテスト駆動開発の回転がもたらすパワーは、矢印のどれかが傷つき短くなっていくことによって、だんだん減っていきます。

では、3つの矢印のうちで、最も打たれ弱く傷つきやすい矢印はどれでしょうか。みなさんは、どう思いますか?

一番弱く傷つきやすいのは、リファクタリングの矢印です。

(中略)

外発的であれ内発的であれ「きれいにしている時間はない」「リファクタリングしている時間はない」などと焦燥感に駆り立てられると、 リファクタリングの時間が短くなったり、リファクタリングが先送りにされ始めます。 リファクタリングの矢印は弱く、折れたり短くなったりしやすいのです。

f:id:nihonbuson:20200706183413p:plain

もちろん、焦燥感によってリファクタリングが行われなくなることは十分ありえます。

ただそれだけなく、「テスト駆動開発」という言葉自体が、リファクタリングという微調整を繰り返す意味を無くしてしまっていることも原因のように思えます。*3

都合の良いように解釈しているだけ?

いや、ちょっと待ってください。

もしかしたら「微調整を繰り返す」という意味を「駆動」という言葉から無くしているのは、都合の良いように解釈しているからかもしれません。

「駆動」の元々の意味は、最初に書いたように「動力を伝えて動かすこと」です。これは「微調整」や「きっかけ」などの場面に限定した意味ではないはずです。

とするならば、いつからか「テスト駆動開発=テストをきっかけとして開発すること」と認識してしまい、そこから逆算的に「駆動=きっかけ」という本来の意味とは少し異なる考えに至ってしまったのかもしれないです。

改めて「テスト駆動開発」の意味を考えてみよう(+宣伝)

私はこの一件で、改めて「テスト駆動開発」というものを意味から考え直すことができました。

皆さんも「テスト駆動開発」とは何か、改めて考えてみませんか?

そもそも「テスト駆動開発」を知らない、やったことないという人は、8/1にTDD Boot Campが久々に開催されるので、そちらに参加して体験してみてはいかがでしょうか?*4

peatix.com

追記

記事を公開したら早くもフィードバックが!ありがたやー。

確かに「driven」と受動態になっている以上、「テストに駆動される開発」と考えた方が良さそうですね。

ただその解釈であっても、「駆動される」タイミングが新しいテストを書き始めた時だけの認識になり、リファクタリングのような微調整を伴う時の認識が薄れているなーという思いは変わりません。*5

確かにそうですね。「誤認の可能性がある」という方が納得感があります。

ただ、発端となった社内の人の発言は「誤訳」なので、記事のタイトルはこのままでいきます。

*1:書籍の例はXPの話であり、TDDに限定した話を示したわけではないですが、TDDにも通じる話のように思ってます

*2:「テストをきっかけとして開発を行う」はテストファーストな開発ではありますが、テスト駆動開発ではないのでは?というのが私の考えです。

*3:「誤訳」ではないが「誤認をしてしまう恐れがある」といったところでしょうか

*4:自分が参加した時のレポートこちら

nihonbuson.hatenadiary.jp

*5:認識が薄れている理由が「駆動」という単語のチョイスではなく、都合の良いように解釈した結果であることは、元の文章でも書いた通りです。