みなさん、Serializable のテストってどうしてますか??
え、やってない?! まさか、そんな、ご冗談を...
java.io.Serializable の実装は、見かけに反してとても難しいです。(少なくとも私はそう感じます。)
どのような実装ミスの危険があるのか、どのようなテストをすべきなのか。
本稿から何回かに分けて、Serializable 実装クラスに対する JUnit テストについて考えてみたいと思います。
おさらい:シリアライズ/デシリアライズ
クラスのインスタンスが使われている間、そのオブジェクトはヒープメモリ上に展開されています。このメモリ上に "モヤっと" 存在しているオブジェクトを、ファイルに保存したりネットワーク越しに送信したりしようとすると、一列のバイト列に変換してやる必要があります。
これがシリアライズですね。
逆に、オブジェクトをファイルから読み込んだりネットワーク越しに受信したりした場合、一列のバイト列をヒープメモリ上に展開しなおしてやる必要があります。
これがデシリアライズですね。
シリアライズ/デシリアライズは、単にヒープメモリ上のビット列をそのまま並べてやればよいというものではありません。目的のオブジェクトと他のオブジェクトとの参照関係がきちんと復元され、オブジェクトグラフが再構成されなければなりません。
シリアライズ元とデシリアライズ先で、同じバージョンのクラスが存在している保証はありません。バージョン間の互換性についての考慮が必要となります。
ディスクが損傷していたら、あるいはファイルの内容が不正に改竄されていたならば、デシリアライズにより間違った内容のオブジェクトを生成してしまうわけにはいきません。
java.io.Serializable を implements する際は、これらのことを考慮する必要があるわけです。
そして、正しく実装がなされたか、これらの観点に基づいてテストを実施すべきだと考えます。
ケース1:デシリアライズ時のインスタンス制御についてテストする
さて、次のようなシングルトンを意図したクラスがあるとします。
ぱっと見たところ、このクラスのインスタンスが複数生成されることは決してないように見えるかもしれません。
public class OnlyOne implements Serializable {
private static final OnlyOne one = new OnlyOne();
public static OnlyOne getInstance() {
return one;
}
private OnlyOne() {
}
}
しかし、このクラスのインスタンスはいくらでも生成できてしまいます。
実際にやってみましょう。
public class OnlyOneTest {
@Test
public void test() throws IOException, ClassNotFoundException {
OnlyOne one = OnlyOne.getInstance();
OnlyOne two = writeAndRead(one);
// このテストは失敗する。
assertThat(one, sameInstance(two));
}
@SuppressWarnings("unchecked")
private <T> T writeAndRead(T data) throws IOException, ClassNotFoundException {
byte[] bytes;
// シリアライズ
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(data);
bytes = bos.toByteArray();
}
// デシリアライズ
try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis)) {
return (T) ois.readObject();
}
}
}
このテストは失敗します。one
と two
は別々のインスタンスなのです。
デシリアライズの過程で、2つ目のインスタンスが生成されてしまいました。
この例から、インスタンス制御を行っているクラスについては、少なくとも「デシリアライズの際に意図した通りのインスタンス制御が行われることを確認する」というテストをすべきであると言えます。
~~~
本稿ではシングルトンクラスの実装ミスを題材として、Serializable 実装クラスに対して行うべきテストの一端を明らかにしました。
次稿では「ケース2:不正なバイト配列からのデシリアライズをテストする」について取り上げます。
蛇足
そもそも、OnlyOne クラスをどう実装すべきだったのか?
それについては、今更紹介する必要もない、こちらの書籍で詳細に解説されています。
Amazon: Effective Java 第2版
なお、上記の書籍でも論じられているように、そもそものそもそも、OnlyOne は enum で実装されるべきです。本稿では分かり易い例として、ふつうのクラスで実装しました。
~~~
OnlyOne よりもう少し複雑な例として、以前の投稿『リバーシで遊んで覚える Java 8.』 で紹介したソースの中の Pointクラス を紹介させていただきます。
このクラスはオセロ盤上の位置を表し、Point.of(i, j)
メソッドは同じ座標に対して常に同じインスタンスを返します。
シリアライズ/デシリアライズを経てもちょうど 8 × 8 = 64 だけのインスタンスが存在するように、シリアライズプロキシパターンを利用してインスタンス制御を行っています。
~~~
nmby はお仕事上 Serializable を使うことが殆どないので、正直なところ、知識も経験も乏しいです。専門的な知見を持つ皆さまによる間違いの指摘やアドバイスなどをいただけますと、とても有難いです。