java.io.Serializable 実装クラスに対してどのようなテストを行うべきなのかを考えるシリーズの中締めです。
これまで3回にわたり、Serializable 実装クラスに対して必要なテストについて実例を交えて見てきました。
- ケース1:デシリアライズ時のインスタンス制御についてテストする
- ケース2:不正なバイト配列からのデシリアライズをテストする
- ケース3:そのクラス、本当に Serializable ですか?- 最低限、シリアライズ/デシリアライズしてみる
今回は中締めとして、これまでの内容をまとめるとともに、さらなる検討課題について明らかにします。
#これまでのまとめ
・Serializable を実装したなら、最低限、一度はシリアライズ/デシリアライズしてみる
ケース3で見たように、単に Serializable を implements しただけでは、オブジェクトがシリアライズ/デシリアライズ可能にならない場合があります。
このような実装不備は、オブジェクトを実際にシリアライズ/デシリアライズしてみることで比較的簡単に見つけることができます。
assertThat(TestUtil.writeAndRead(new OthelloBoard()), instanceOf(OthelloBoard.class));
assertThat(TestUtil.writeAndRead(new Odd(7)).get(), is(7));
・デシリアライズ時のインスタンス制御について確認する
ケース1で見たように、デシリアライズによるオブジェクト生成はコンストラクタを迂回します。
シングルトンやそれに類するインスタンス制御を行っているクラスについては、デシリアライズ時に意図したインスタンス制御が行われることを確認する必要があります。
assertThat(TestUtil.writeAndRead(OnlyOne.getInstance()), sameInstance(OnlyOne.getInstance()));
assertThat(TestUtil.writeAndRead(Point.of(1, 2)), sameInstance(Point.of(1, 2)));
・不正に改竄されたバイト配列からの防御をテストする
ケース2で見たように、シリアライズされたバイト配列を読み解いて改竄するのは比較的簡単であり、そのような不正に備えるのは Serializable 実装クラスの責任です。
実際に不正に改竄したバイト配列を与えてデシリアライズし、意図した防御がなされるかを確認するべきです。
// test1
Function<byte[], byte[]> modifier1 = original -> {
byte[] modified = Arrays.copyOf(original, original.length);
modified[original.length - 1] = 0x02;
return modified;
};
assertThat(Testee.of(() -> TestUtil.writeModifyAndRead(new Odd(7), modifier1)),
raise(FailToDeserializeException.class)
.rootCause(InvalidObjectException.class), "illegal value. n == 2");
// test2
Function<byte[], byte[]> modifier2 = original -> {
return TestUtil.replace(original,
TestUtil.bytes(MyClass.class.getName() + "$SerializationProxy"),
TestUtil.bytes(MyClass.class.getName()));
};
assertThat(Testee.of(() -> TestUtil.writeModifyAndRead(new MyClass(), modifier2)),
raise(FailToDeserializeException.class)
.rootCause(InvalidObjectException.class, "proxy required"));
なお、上記のコード例で使用した Testee.of
, raise
, rootCause
は、以前の投稿『JUnit4での例外テストを楽チンにする!』で紹介したライブラリの機能です。
TestUtil
は今回作成したものであり、別稿で取り上げる予定です。⇒ 投稿しました:『JUnit4でのシリアライズ/デシリアライズ検証を楽チンにする!』
#さらなる検討課題
・異なるバージョン間の互換性の検証
古いバージョンで保存したファイルを、新しいバージョンで読み込んだら?
ネットワーク越しの送信先で、古いバージョンのクラスが動いていたら?
異なるバージョン間の互換/非互換を適切に制御することは、Serializable 実装にまつわる大きなテーマのひとつです。
これを JUnit でテストしようとした場合、クラスローダを駆使して異なるバージョンのクラスを同一 JVM 上にロードする、なんてことになるのでしょうか。
現在の私の知識では太刀打ちできないため、このテーマについては将来に譲ることにします。
・セキュリティに関する考慮
ケース2で見たように、シリアライズされたバイト配列では private なフィールドの内容も丸見えです。従って、センシティブなデータはシリアライズ対象外とするか、暗号化してシリアライズする必要があります。
アプリケーションの内容によっては、このような考慮と検証が必要になるのでしょう。
~ ~ ~
ここまで、nmby の拙い知識に基づき Serializable 実装クラスに対して行うべきテストについて整理してきました。
nmby はお仕事で Serializable 実装クラスを扱うことが殆ど無いのですが、ソフトウェアを一般販売しているような企業では、きっと専門的な知見が蓄積され、方法論が確立されているのでしょうね。
専門的な知見を持つ皆さまによる間違いの指摘やアドバイス、ツッコミなどをお待ちしております。