即使最杰出的开发人员有时也会忘记测试对象串行化,但那并不能作为您犯下同一错误的借口。在这篇文章中,Elliotte
Rusty Harold 将解释对对象串行化进行单元测试的重要性,并为您展示一些应牢记的测试。
测试驱动的开发的总体原则之一就是应测试一个类已发布的所有接口。如果客户机能够调用方法或访问字段,那么就测试它。但在 Java™
语言中,许多类都有一个已发布的接口容易被遗漏:通过类实例生成的串行化对象。有时这些类显式实现 Serializable 。而有时则是直接从超类继承这一特性。在任何一种情况下,您都应该测试其串行化形式。本文将介绍几种测试对象串行化的方法。
对串行化来说,测试极其重要,因为串行化非常非常容易出错。在修复 bug 或优化类时,非常容易破坏所有已有串行化对象。如果您在更改代码时未考虑串行化,几乎可以肯定您必将破坏原有对象。若您正在为任何形式的持久性存储使用串行化,那么这将是一个严重的
bug。即便仅为流程间的瞬时消息传递(如在 RMI 中)使用对象串行化,更改串行化格式也会使那些各类的版本不完全相同的系统无法顺利交换数据。
幸运的是,若您谨慎对待串行化问题,在处理类时通常可以避免不兼容的更改。Java 语言提供了多种方法,可维护一个类的不同版本之间的兼容性,包括:
serialVersionUID
transient 修饰符
readObject() 和
writeObject()
writeReplace()
和 readResolve()
serialPersistentFields
对于这些解决方案来说,最大的问题就在于程序员未使用它们。当您将精力集中在修复 bug、添加特性或解决性能问题时,往往不会停下来思考您的更改对串行化造成的影响。然而串行化是一个涉及范围极广的问题
—— 跨越一个系统的多个不同层。几乎所有更改都会涉及对串行化有某种影响的一个类的实例字段。这正是单元测试发挥作用的时机。在本文后续各节中,我将为您展示一些简单的单元测试,这些单元测试能确保您不会不经意地更改可串行化类的串行格式。
通常您编写的第一个串行化测试就是用于验证串行化是否可行的测试。即使一个类实现了 Serializable ,依然不能保证它能够串行化。例如,如果一个可串行化的容器(如
ArrayList )包含一个不可串行化的对象(如 Socket ),则在您尝试串行化此容器时,将抛出
NotSerializableException 。
通常,对此测试,您只需在 ByteArrayOutputStream 上写入数据。若未抛出任何异常,测试即通过。如果您愿意,还可测试一些已写入的输出。例如,清单
1 所示代码片段用于测试 Jaxen 的 BaseXPath 类是否可串行化:
清单 1. 此类是否可串行化?
public void testIsSerializable()
throws JaxenException, IOException {
BaseXPath path = new BaseXPath("//foo", new DocumentNavigator());
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(path);
oos.close();
assertTrue(out.toByteArray().length > 0);
}
|
接下来,您想要编写一个测试,不仅要验证输出得到了显示,还要验证输出是正确的。您可通过两种方式完成这一任务:
- 反串行化对象,并将其与原始对象相比较。
- 逐字节地将其与参考 .ser 文件相比较。
我通常会从第一种选择入手,因为它还提供了一个反串行化的简单测试,而且编码和实现相对来说比较容易。例如,清单 2 所示代码片段将测试
Jaxen 的 SimpleVariableContext 类是否可写入并在之后重新读回:
清单 2. 反串行化对象,并将其与原始对象相比较
public void testRoundTripSerialization()
throws IOException, ClassNotFoundException, UnresolvableException {
// construct test object
SimpleVariableContext original = new SimpleVariableContext();
original.setVariableValue("s", "String Value");
original.setVariableValue("x", new Double(3.1415292));
original.setVariableValue("b", Boolean.TRUE);
// serialize
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(original);
oos.close();
//deserialize
byte[] pickled = out.toByteArray();
InputStream in = new ByteArrayInputStream(pickled);
ObjectInputStream ois = new ObjectInputStream(in);
Object o = ois.readObject();
SimpleVariableContext copy = (SimpleVariableContext) o;
// test the result
assertEquals("String Value", copy.getVariableValue("", "", "s"));
assertEquals(Double.valueOf(3.1415292), copy.getVariableValue("", "", "x"));
assertEquals(Boolean.TRUE, copy.getVariableValue("", "", "b"));
assertEquals("", "");
}
|
让我们再试一次……
在测试代码基础中那些此前从未测试过的部分时,几乎总是会发现 bug,对象串行化也是这样。在我第一次运行清单 2 中的测试时,测试失败了,输出结果如清单
3 所示:
清单 3. 不可串行化
java.io.NotSerializableException:
org.jaxen.QualifiedName
at
java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1075)
at
java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:291)
at java.util.HashMap.writeObject(HashMap.java:984)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at
java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:890)
at
java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1333)
at
java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1284)
at
java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1073)
at
java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1369)
at
java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1341)
at
java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1284)
at
java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1073)
at
java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:291)
at
org.jaxen.test.SimpleVariableContextTest.testRoundTripSerialization
(SimpleVariableContextTest.java:90)
|
这表明,SimpleVariableContext 包含一个对
QualifiedName 对象的引用,QualifiedName
类未标记为 Serializable 。我为 QualifiedName
的类签名添加了 implements Serializable ,这一次测试顺利通过。
注意,此测试实际上并未验证串行化格式是否正确 —— 只是验证出对象能够来回转换。为测试正确性,您需要生成一些参考文件,以便与类的所有未来版本的输出相比较。
通常,您不能依赖默认串行化格式来保持类的不同版本间的文件格式兼容性。您必须使用 serialPersistentFields 、readObject()
和 writeObject() 方法和/或 transient 修饰符,通过各种方式进行定制。如果您确实对类的串行化格式做出了不兼容的更改,应相应更改
serialVersionUID 字段,以指出您这样做了。
正常情况下,您不会过分关注串行化对象的详细结构。而只是关注最初使用的那种格式随着类的发展得到了维护。一旦类基本上具备了恰当的形式,即可写入一些类的串行化实例,并存储在随后可将其作为参考使用的位置处。(您很可能确实希望多多少少地考虑如何串行化才能确保足够的灵活性,以便应对未来的发展。)
编写串行化实例的程序是临时代码,只需使用一次。实际上,您根本就不应该多次运行这段代码,因为您不希望获得串行化格式中的任何意外更改。例如,清单
4 展示了用于串行化 Jaxen 的 SimpleVariableContext 类的程序:
清单 4. 写入串行化实例的程序
import org.jaxen.*;
import java.io.*;
public class MakeSerFiles {
public static void main(String[] args) throws IOException {
OutputStream fout = new FileOutputStream("xml/simplevariablecontext.ser");
ObjectOutputStream out = new ObjectOutputStream(fout);
SimpleVariableContext context = new SimpleVariableContext();
context.setVariableValue("s", "String Value");
context.setVariableValue("x", new Double(3.1415292));
context.setVariableValue("b", Boolean.TRUE);
out.writeObject(context);
out.flush();
out.close();
}
}
|
您只需将一个串行化对象写入文件 —— 而且只需一次。这是您希望保存的文件,而不是用于写入的代码。清单 5 展示了 Jaxen
的 SimpleVariableContext 类的兼容性测试:
清单 5. 确保文件格式未被更改
public void testSerializationFormatHasNotChanged()
throws IOException, ClassNotFoundException, UnresolvableException {
//deserialize
InputStream in = new FileInputStream("xml/simplevariablecontext.ser");
ObjectInputStream ois = new ObjectInputStream(in);
Object o = ois.readObject();
SimpleVariableContext context = (SimpleVariableContext) o;
// test the result
assertEquals("String Value", context.getVariableValue("", "", "s"));
assertEquals(Double.valueOf(3.1415292), context.getVariableValue("",
"", "x"));
assertEquals(Boolean.TRUE, context.getVariableValue("", "", "b"));
assertEquals("", "");
}
|
默认情况下,类通常是可串行化的。例如,java.lang.Throwable 或 java.awt.Component
的任何子类都会从其祖先继承可串行性。在某些情况下,这也是您希望的结果,但并非总是如此。有的时候,串行化可能会成为安全漏洞,使恶意程序员能够在不调用构造函数或
setter 方法的情况下创建对象,从而规避了您小心翼翼地在类中构建的所有约束性检查。
若您希望类可串行化,就需要测试它,这与您需要测试一个直接实现了 Serializable 的类相同。如果您不希望类可串行化,则应重写
writeObject() 和 readObject() ,使两者均抛出 NotSerializableException ,随后您也需要对其进行测试。
此类测试的实现方法与其他任何 JUnit 异常测试相似。只需在应抛出异常的语句两端包围一个 try
块即可,随后紧接欲抛出异常的语句之后添加一条 fail() 语句。如果愿意,您还可在 catch
中作出一些关于所抛出异常的断言。例如,清单 6 验证了 FunctionContext 是不可串行化的:
清单 6. 测试 FunctionContext 是不可串行化的
public void testSerializeFunctionContext()
throws JaxenException, IOException {
DOMXPath xpath = new DOMXPath("/root/child");
FunctionContext context = xpath.getFunctionContext();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(out);
try {
oout.writeObject(context);
fail("serialized function context");
}
catch (NotSerializableException ex) {
assertNotNull(ex.getMessage());
}
}
|
Java 5 和 JUnit 4 使异常测试更为轻松。只需在 @Test 注释中声明所需异常即可,如清单
7 所示:
清单 7. 带有注释的异常测试
@Test(expected=NotSerializableException.class) public
void testSerializeFunctionContext()
throws JaxenException, IOException {
DOMXPath xpath = new DOMXPath("/root/child");
FunctionContext context = xpath.getFunctionContext();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(out);
oout.writeObject(context);
}
|
串行化格式可以说是代码基础中最脆弱、健壮性最差的部分。有的时候,似乎只要以奇异的眼神盯着它,它就会被破坏。单元测试和测试驱动的开发这些出色的工具使您可以信心十足地管理此类脆弱系统
—— 但只有在您确实使用了这些工具时,它们才能发挥作用。
若您关注对象串行化,特别是希望为长期持久性存储使用串行化对象时,就必须对串行化进行测试。不要假设您的 Java 代码所做的一切都是正确的
—— 它很可能会出错!如果您将串行化测试作为测试套件的固定部分,则维护长期兼容性就会更轻松。您花费在对象串行化单元测试上的时间将为您带来成倍的回报,此后调试时您能节省的时间将数倍于投入时间。
学习
获得产品和技术
讨论
|