プライベートフィールドに対するテスト

164件のシェア(すこし話題の記事)

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

クラスメソッドの渡辺です。

弊社では業務時間内にブログを書くことが推奨されており、ネタも何でも良いということで、自動化やユニットテスト関連も投下していきます。今日は結構良く話題に出るプライベートフィールドに対するテストです。

オブジェクト指向プログラミングと可視性

オブジェクト指向プログラミングのひとつの特徴はカプセル化です。簡単に言えば、フィールド(情報)やメソッド(機能や操作)の公開範囲を可能な限り狭くすることで、安全にオブジェクトを扱うことができる、ということです。このため、古典的なJavaのコーディング標準では、次のように、「全てのフィールドをprivateに設定し、必要に応じてアクセサメソッドを定義すること」となっています。

public class Item {
    private String name;
    private int price;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
}

しかし、このような書き方は面倒で、無駄にコード量も増えてしまうため、モダンなコーディング標準では「可能な限りスコープは狭く」程度になっているでしょう(IDEで自動生成するので手で書くことはありませんが…)。このため、単純なデータクラスであれば、次のようにpublicフィールドにすることも許可して良いでしょう。

public class Item {
    public String name;
    public int price;
}

Javaだって進化が止まっているわけではなく、書き方も時代に合わせてより良い書き方を模索しているんです。

プライベートフィールドと状態

さて、プライベートフィールドがある場合、ユニットテスト時に困る事があります。それは、なんらかの操作(メソッドの実行)を行った時に、プライベートフィールドの値が期待した通りに変化していることを検証したい場合に、どうやってプライベートフィールドを参照するかという問題です。

例えば、次のようなカウントを行うクラスのテストを書いてみましょう。

public class Counter {
    private int count;
    public void countUp() {
        count++;
    }
}

オブジェクトの初期化時にcountが0であること、countUpメソッドは実行後にcountが1増えることなどをテストしようにも、countフィールドにアクセスできません。

// コンパイルエラーとなる
public class CounterTest {

    @Test
    public void オブジェクト生成時にcountが0であること() {
        Counter sut = new Counter();
        assertThat(sut.count, is(0));
    }

    @Test
    public void countUpを実行するとcountが1であること() {
        Counter sut = new Counter();
        sut.countUp();
        assertThat(sut.count, is(1));
    }
}

テストを行う場合はここで悩まないでしょうが、テストコードを書こうとすると幾つかの選択肢があることに気付きます。

アクセサを定義する

ひとつ目の解決方法としては、countフィールドに対するアクセサメソッドを定義することです。次のようにgetCountメソッドを定義すれば、各テストケースでの検証ができるようになります。

public class Counter {
    private int count;
    public void countUp() {
        count++;
    }
    // 追加したアクセサメソッド
    public int getCount() {
        return count;
    }
}

テストコードはこうなります。

public class CounterTest {

    @Test
    public void オブジェクト生成時にcountが0であること() {
        Counter sut = new Counter();
        assertThat(sut.getCount(), is(0));
    }

    @Test
    public void countUpを実行するとcountが1であること() {
        Counter sut = new Counter();
        sut.countUp();
        assertThat(sut.getCount(), is(1));
    }
    
}

この方法は、オブジェクト指向プログラミングとしても最も正攻法であると言えるでしょう。Counterクラスではcountを取得するメソッドがあることも妥当な設計と言えるため、違和感もありません。適切にカプセル化されています。

しかし、フィールドの公開があまり妥当でない場合、ユニットテストのためにアクセサを定義することには違和感を感じます。アクセサが操作かどうかは意見もわかれると思いますが、可能であれば無駄なメソッドを定義したくありません。メソッドが多いと言うことはメンテナンス性に影響を与えるからです。

リフレクションAPIを使う

そこで、なんとかプライベートフィールドにそのままアクセスできないかと考えると、リフレクションAPIを使えばできることに気付きます。リフレクションAPIを使い、フィールドを取得するには次のようにすれば良いです。

public class CounterTest {

    @Test
    public void オブジェクト生成時にcountが0であること() throws Exception {
        Counter sut = new Counter();
        int actualCount = getCountByReflection(sut);
        assertThat(actualCount, is(0));
    }

    @Test
    public void countUpを実行するとcountが1であること() throws Exception {
        Counter sut = new Counter();
        sut.countUp();
        int actualCount = getCountByReflection(sut);
        assertThat(actualCount, is(1));
    }

    int getCountByReflection(Counter obj) throws Exception {
        Field field = Counter.class.getDeclaredField("count");
        field.setAccessible(true);
        return field.getInt(obj);
    }

}

リフレクションAPIを利用すれば、プロダクションコードに手を加えなくて済むので、ベターな方法とも思えます。 しかし、「わざわざそんな面倒なことをしなくても・・・だからJavaは・・・」。ですよねー。

パッケージプライベートを使う

なるべくフィールドの可視性は狭く、メソッドも増やしたくない、というならば、フィールドの可視性をパッケージプライベートとすることがオススメです。パッケージプライベート(アクセス修飾子無し)は、privateの次に可視性の狭く、同一パッケージからのアクセスを許可しますが、他のパッケージからのアクセスは許可しません。JUnitの作法を持ってテストコードを書くならば、テストクラスとテスト対象クラスは同じパッケージにあるはずです。

public class Counter {
    int count;
    public void countUp() {
        count++;
    }
}

countがパッケージプライベートなので、自然にテストコードからアクセスできます。

public class CounterTest {

    @Test
    public void オブジェクト生成時にcountが0であること() throws Exception {
        Counter sut = new Counter();
        assertThat(sut.count, is(0));
    }

    @Test
    public void countUpを実行するとcountが1であること() throws Exception {
        Counter sut = new Counter();
        sut.countUp();
        assertThat(sut.count, is(1));
    }

}

パッケージプライベートを使えば、プロダクションコードもテストコードも可読性を保ちつつ、カプセル化も大きく崩さないことが解ると思います。

「もし、チーム内に悪意を持ったプログラマが居て、パッケージプライベートのフィールドを利用して危険なことを行ったらどうするんだ?」というツッコミがあったならば、「そのようなプログラマをチームに入れないでください」と答えましょう:p

というわけで、ユニットテストでプライベートフィールドを参照したい時のベストプラクティスを紹介しました。

Have a nice testing!

  • Laurent Fabre

    refrection→reflectionです。
    private宣言に影響されないテストのもう一つの方法として、テストをGroovyで書くという手があります。Javaに限定した話であれば論外ですが。
    なんと言っても本来privateにしておきたいフィールドをわざわざテストのために別の修飾子に変えるっていうのは気持ちの良いことではありませんね。

  • SHUJI

    > Laurent Fabreさん
    コメントあ。りがとうございます、スペルミスは修正しておきました。
    Groovyについてはメリットも多いと思います。そのへんの話題もいずれ書こうと思います。

    privateフィールドについてですが、自分は無理にprivateにする必要も感じません。ノーガード(public)は流石に気持ち悪いですが、デフォルトスコープにしておけば、十分なカプセル化を行っているという認識です。
    また、テストのためにアクセス修飾子を変更したり、テストのためにAPI設計を変更したり、テストのためにプロダクションコードを変更することには抵抗はないですね。テストしやすい方が結果的に良いプロダクトコードになるという考え方です。