测试对象串行化
上一篇 / 下一篇 2007-08-30 11:20:14 / 个人分类:测试知识
Elliotte Harold(elharo@metalab.unc.edu), 副教授, Polytechnic University
7[Q
h\WL1YK051Testing软件测试网:h#AQLI
bN^7x#r9J
/LHy%l0~?02006 年 7 月 06 日
b Jecfys#s?0即使最杰出的开发人员有时也会忘记测试对象串行化,但那并不能作为您犯下同一错误的借口。在这篇文章中,Elliotte Rusty Harold 将解释对对象串行化进行单元测试的重要性,并为您展示一些应牢记的测试。51Testing软件测试网A0w#KK7[
测试驱动的开发的总体原则之一就是应测试一个类已发布的所有接口。如果客户机能够调用方法或访问字段,那么就测试它。但在 Java™ 语言中,许多类都有一个已发布的接口容易被遗漏:通过类实例生成的串行化对象。有时这些类显式实现Serializable
。而有时则是直接从超类继承这一特性。在任何一种情况下,您都应该测试其串行化形式。本文将介绍几种测试对象串行化的方法。
对串行化来说,测试极其重要,因为串行化非常非常容易出错。在修复 bug 或优化类时,非常容易破坏所有已有串行化对象。如果您在更改代码时未考虑串行化,几乎可以肯定您必将破坏原有对象。若您正在为任何形式的持久性存储使用串行化,那么这将是一个严重的 bug。即便仅为流程间的瞬时消息传递(如在 RMI 中)使用对象串行化,更改串行化格式也会使那些各类的版本不完全相同的系统无法顺利交换数据。
"_qi$d3S\Y01F5Sw;[Gq(R%C5he5B0幸运的是,若您谨慎对待串行化问题,在处理类时通常可以避免不兼容的更改。Java 语言提供了多种方法,可维护一个类的不同版本之间的兼容性,包括:51Testing软件测试网JL0uV2t
serialVersionUID
transient
修饰符readObject()
和writeObject()
writeReplace()
和readResolve()
serialPersistentFields
对于这些解决方案来说,最大的问题就在于程序员未使用它们。当您将精力集中在修复 bug、添加特性或解决性能问题时,往往不会停下来思考您的更改对串行化造成的影响。然而串行化是一个涉及范围极广的问题 —— 跨越一个系统的多个不同层。几乎所有更改都会涉及对串行化有某种影响的一个类的实例字段。这正是单元测试发挥作用的时机。在本文后续各节中,我将为您展示一些简单的单元测试,这些单元测试能确保您不会不经意地更改可串行化类的串行格式。51Testing软件测试网ZLJ} ?.NO'J,em
51Testing软件测试网 D%J ]m4m|~;[Glk ]0@*N_}7A [0 |
51Testing软件测试网 T%x~g?R'~*@%w0K.l
|
EEU;u1M@ k5k^0
fS` ]F/e6qx }0我能否将其串行化?
0^UG-` aIl7Y0BX Xf A \0通常您编写的第一个串行化测试就是用于验证串行化是否可行的测试。即使一个类实现了Serializable
,依然不能保证它能够串行化。例如,如果一个可串行化的容器(如ArrayList
)包含一个不可串行化的对象(如Socket
),则在您尝试串行化此容器时,将抛出NotSerializableException
。
通常,对此测试,您只需在ByteArrayOutputStream
上写入数据。若未抛出任何异常,测试即通过。如果您愿意,还可测试一些已写入的输出。例如,清单 1 所示代码片段用于测试 Jaxen 的BaseXPath
类是否可串行化:
K6as;B)w7@0清单 1. 此类是否可串行化?
[7qW[pl{0
public void testIsSerializable() throws JaxenException, IOException { BaseXPath path = new BaseXPath("//foo", new DocumentNavigator()); ByteArrayOutputStream ōut = new ByteArrayOutputStream(); ObjectOutputStream ōos = new ObjectOutputStream(out); oos.writeObject(path); oos.close(); assertTrue(out.toByteArray().length > 0); } |
51Testing软件测试网 U)r d"_4v
tY?2}J/}0 |
51Testing软件测试网zy#?)Opp
Y6H |
51Testing软件测试网(f QT-[S2`
51Testing软件测试网x S+ONcc1T`
测试串行化形式51Testing软件测试网 p ~,Cyk n
l^5~8]!Q$K^\/b}0接下来,您想要编写一个测试,不仅要验证输出得到了显示,还要验证输出是正确的。您可通过两种方式完成这一任务:51Testing软件测试网l`NyT+\x
- 反串行化对象,并将其与原始对象相比较。
- 逐字节地将其与参考 .ser 文件相比较。
我通常会从第一种选择入手,因为它还提供了一个反串行化的简单测试,而且编码和实现相对来说比较容易。例如,清单 2 所示代码片段将测试 Jaxen 的SimpleVariableContext
类是否可写入并在之后重新读回:51Testing软件测试网tw p'K2MtbX
清单 2. 反串行化对象,并将其与原始对象相比较51Testing软件测试网Z Q8YVKeOJ-n%R!Wxo
public void testRoundTripSerialization() throws IOException, ClassNotFoundException, UnresolvableException { // construct test object SimpleVariableContext ōriginal = new SimpleVariableContext(); original.setVariableValue("s", "String Value"); original.setVariableValue("x", new Double(3.1415292)); original.setVariableValue("b", Boolean.TRUE); // serialize ByteArrayOutputStream ōut = new ByteArrayOutputStream(); ObjectOutputStream ōos = new ObjectOutputStream(out); oos.writeObject(original); oos.close(); //deserialize byte[] pickled = out.toByteArray(); InputStream in = new ByteArrayInputStream(pickled); ObjectInputStream ōis = 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("", ""); } |
51Testing软件测试网+N'VE0SL^
让我们再试一次……51Testing软件测试网`5l y3[-G1l6C*P
~\VI;l,Y0在测试代码基础中那些此前从未测试过的部分时,几乎总是会发现 bug,对象串行化也是这样。在我第一次运行清单 2 中的测试时,测试失败了,输出结果如清单 3 所示:51Testing软件测试网)^ehi"^
ML4Dh2b`j L0清单 3. 不可串行化
D%[3{&Pm0
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) |
51Testing软件测试网 g]k'N;|aK!e"K'n
这表明,SimpleVariableContext
包含一个对QualifiedName
对象的引用,QualifiedName
类未标记为Serializable
。我为QualifiedName
的类签名添加了implements Serializable
,这一次测试顺利通过。
注意,此测试实际上并未验证串行化格式是否正确 —— 只是验证出对象能够来回转换。为测试正确性,您需要生成一些参考文件,以便与类的所有未来版本的输出相比较。
d I3z2Uj'qC051Testing软件测试网f2_#zX8xvJJ'g/s51Testing软件测试网]{2Ya$@ T |
51Testing软件测试网 L3B$Dl`q |
c:]gp4T2\ a6I0
4|,i jt3I1wD:j+xIj0测试反串行化
hwY&n,rf#I051Testing软件测试网#Gk#n_GcZ3_{通常,您不能依赖默认串行化格式来保持类的不同版本间的文件格式兼容性。您必须使用serialPersistentFields
、readObject()
和writeObject()
方法和/或transient
修饰符,通过各种方式进行定制。如果您确实对类的串行化格式做出了不兼容的更改,应相应更改serialVersionUID
字段,以指出您这样做了。
正常情况下,您不会过分关注串行化对象的详细结构。而只是关注最初使用的那种格式随着类的发展得到了维护。一旦类基本上具备了恰当的形式,即可写入一些类的串行化实例,并存储在随后可将其作为参考使用的位置处。(您很可能确实希望多多少少地考虑如何串行化才能确保足够的灵活性,以便应对未来的发展。)
1{)M.e2C:wdn5d0&l]T A-[0编写串行化实例的程序是临时代码,只需使用一次。实际上,您根本就不应该多次运行这段代码,因为您不希望获得串行化格式中的任何意外更改。例如,清单 4 展示了用于串行化 Jaxen 的SimpleVariableContext
类的程序:
清单 4. 写入串行化实例的程序51Testing软件测试网(C wZ2u*D^i
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 ōut = 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(); } } |
g'ZD*`H0
nRr2k|(n0您只需将一个串行化对象写入文件 —— 而且只需一次。这是您希望保存的文件,而不是用于写入的代码。清单 5 展示了 Jaxen 的SimpleVariableContext
类的兼容性测试:
清单 5. 确保文件格式未被更改
y'Si)Sea7B+j0
public void testSerializationFormatHasNotChanged() throws IOException, ClassNotFoundException, UnresolvableException { //deserialize InputStream in = new FileInputStream("xml/simplevariablecontext.ser"); ObjectInputStream ōis = 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("", ""); } |
,j%c"l0EM051Testing软件测试网)smT1N3sg~
51Testing软件测试网\8P"?4D
X-v |
#B.S ^0E+K!P/x&kBv%a0
|
K8U d2q._~6S8]#s0
9m E$i2eUI!j!l6d0测试不可串行性
kOf3jB4j-_K0g0V!FCs_@UFn?0默认情况下,类通常是可串行化的。例如,java.lang.Throwable
或java.awt.Component
的任何子类都会从其祖先继承可串行性。在某些情况下,这也是您希望的结果,但并非总是如此。有的时候,串行化可能会成为安全漏洞,使恶意程序员能够在不调用构造函数或 setter 方法的情况下创建对象,从而规避了您小心翼翼地在类中构建的所有约束性检查。
若您希望类可串行化,就需要测试它,这与您需要测试一个直接实现了Serializable
的类相同。如果您不希望类可串行化,则应重写writeObject()
和readObject()
,使两者均抛出NotSerializableException
,随后您也需要对其进行测试。
&q,iLW2}0此类测试的实现方法与其他任何 JUnit 异常测试相似。只需在应抛出异常的语句两端包围一个try
块即可,随后紧接欲抛出异常的语句之后添加一条fail()
语句。如果愿意,您还可在catch
中作出一些关于所抛出异常的断言。例如,清单 6 验证了FunctionContext
是不可串行化的:51Testing软件测试网2k0b-o$y)P[
bK~
AQU
o7D%i5a\\0清单 6. 测试 FunctionContext 是不可串行化的
_rS{6~ A0
public void testSerializeFunctionContext() throws JaxenException, IOException { DOMXPath xpath = new DOMXPath("/root/child"); FunctionContext context = xpath.getFunctionContext(); ByteArrayOutputStream ōut = new ByteArrayOutputStream(); ObjectOutputStream ōout = new ObjectOutputStream(out); try { oout.writeObject(context); fail("serialized function context"); } catch (NotSerializableException ex) { assertNotNull(ex.getMessage()); } } |
(x6i:\1uiZ9n2lR0Java 5 和 JUnit 4 使异常测试更为轻松。只需在@Test
注释中声明所需异常即可,如清单 7 所示:51Testing软件测试网o1{N6T,NAP
清单 7. 带有注释的异常测试
2K[.cq sT/k{-{8g0
@Test(expected=NotSerializableException.class) public void testSerializeFunctionContext() throws JaxenException, IOException { DOMXPath xpath = new DOMXPath("/root/child"); FunctionContext context = xpath.getFunctionContext(); ByteArrayOutputStream ōut = new ByteArrayOutputStream(); ObjectOutputStream ōout = new ObjectOutputStream(out); oout.writeObject(context); } |
*l+uK3AzT8V/X051Testing软件测试网2eIw/h6bU
51Testing软件测试网8S?LF R6@4G |
51Testing软件测试网!G7GA
A"gE2Py
|
%A@9w K'O}0
^qQ&P,G;U,E051Testing软件测试网#[*[G'[!LXv
结束语51Testing软件测试网)oY O0C WG
51Testing软件测试网Vamf(o串行化格式可以说是代码基础中最脆弱、健壮性最差的部分。有的时候,似乎只要以奇异的眼神盯着它,它就会被破坏。单元测试和测试驱动的开发这些出色的工具使您可以信心十足地管理此类脆弱系统 —— 但只有在您确实使用了这些工具时,它们才能发挥作用。
#z1A6ygJh R"U.V0+O[,RY#K[j-b _0若您关注对象串行化,特别是希望为长期持久性存储使用串行化对象时,就必须对串行化进行测试。不要假设您的 Java 代码所做的一切都是正确的 —— 它很可能会出错!如果您将串行化测试作为测试套件的固定部分,则维护长期兼容性就会更轻松。您花费在对象串行化单元测试上的时间将为您带来成倍的回报,此后调试时您能节省的时间将数倍于投入时间。
8o)f ?B:`B^051Testing软件测试网sz I i{O'N9l51Testing软件测试网? Fu#S)G%x
?/q$i(nB| ^0参考资料
\$z4[rZ!V0学习51Testing软件测试网F4m#Z ]y,R_.v- 您可以参阅本文在 developerWorks 全球站点上的英文原文。
\'R:`]&tM c0
;~:c0`:aG O0 - “利用 Ant 和 JUnit 进行增量开发”(Malcolm Davis,developerWorks,2000 年 11 月):介绍 Java 平台上的单元测试。51Testing软件测试网`;Q\,IgZe.pZ
51Testing软件测试网9Z{mPnlDb - “揭开极端编程的神秘面纱: 测试驱动的编程”(Roy Miller,developerWorks,2003 年 4 月):介绍关于测试驱动编程的一切,更重要的是 —— 测试驱动编程与什么无关。