IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Java technology | Open source  >

测试对象串行化

容易被遗漏的重要测试

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Elliotte Harold (elharo@metalab.unc.edu), 副教授, Polytechnic University

2006 年 7 月 06 日

即使最杰出的开发人员有时也会忘记测试对象串行化,但那并不能作为您犯下同一错误的借口。在这篇文章中,Elliotte Rusty Harold 将解释对对象串行化进行单元测试的重要性,并为您展示一些应牢记的测试。

测试驱动的开发的总体原则之一就是应测试一个类已发布的所有接口。如果客户机能够调用方法或访问字段,那么就测试它。但在 Java™ 语言中,许多类都有一个已发布的接口容易被遗漏:通过类实例生成的串行化对象。有时这些类显式实现 Serializable。而有时则是直接从超类继承这一特性。在任何一种情况下,您都应该测试其串行化形式。本文将介绍几种测试对象串行化的方法。

测试串行化

对串行化来说,测试极其重要,因为串行化非常非常容易出错。在修复 bug 或优化类时,非常容易破坏所有已有串行化对象。如果您在更改代码时未考虑串行化,几乎可以肯定您必将破坏原有对象。若您正在为任何形式的持久性存储使用串行化,那么这将是一个严重的 bug。即便仅为流程间的瞬时消息传递(如在 RMI 中)使用对象串行化,更改串行化格式也会使那些各类的版本不完全相同的系统无法顺利交换数据。

告诉每一个人。将此提交到:

digg Digg
slashdot Slashdot

幸运的是,若您谨慎对待串行化问题,在处理类时通常可以避免不兼容的更改。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,这一次测试顺利通过。

注意,此测试实际上并未验证串行化格式是否正确 —— 只是验证出对象能够来回转换。为测试正确性,您需要生成一些参考文件,以便与类的所有未来版本的输出相比较。





回页首


测试反串行化

通常,您不能依赖默认串行化格式来保持类的不同版本间的文件格式兼容性。您必须使用 serialPersistentFieldsreadObject()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.Throwablejava.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 代码所做的一切都是正确的 —— 它很可能会出错!如果您将串行化测试作为测试套件的固定部分,则维护长期兼容性就会更轻松。您花费在对象串行化单元测试上的时间将为您带来成倍的回报,此后调试时您能节省的时间将数倍于投入时间。



参考资料

学习
  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文

  • 利用 Ant 和 JUnit 进行增量开发”(Malcolm Davis,developerWorks,2000 年 11 月):介绍 Java 平台上的单元测试。

  • 揭开极端编程的神秘面纱: 测试驱动的编程”(Roy Miller,developerWorks,2003 年 4 月):介绍关于测试驱动编程的一切,更重要的是 —— 测试驱动编程与什么无关。

  • Keeping critters out of your code”(David Carew,Sandeep Desai,Anthony Young-Garner,developerWorks,2003 年 6 月):介绍服务器端应用服务器环境的单元测试。

  • JUnit 4 抢先看”(Elliotte Rusty Harold,developerWorks,2005 年 9 月):介绍 JUnit 4 中基于注释的全新架构,需要 Java 5 或更新版本。

  • Java I/O,第 2 版(Elliotte Rusty Harold;O'Reilly,2006 年 5 月):深入讨论对象串行化的新版图书。

  • Pragmatic Unit Testing(Dave Thomas 和 Andy Hunt;Pragmatic Programmer,2003 年 9 月):单元测试 Java 代码的完整介绍。

  • Java 技术专区:数百篇关于 Java 编程各个方面的文章。

获得产品和技术
  • JUnit:影响您的测试。


讨论


关于作者

Elliotte Rusty Harold 来自新奥尔良, 现在他还定期回老家喝一碗美味的秋葵汤。不过目前,他和妻子 Beth 定居在纽约临近布鲁克林的 Prospect Heights,同住的还有他的猫咪 Charm(取自夸克)和 Marjorie(取自他岳母的名字)。他是 Polytechnic 大学计算机科学系的一名副教授,讲授 Java 和面向对象编程。他的 Cafe au Lait Web 站点已经成为 Internet 上最流行的独立 Java 站点之一,它的姊妹站点 Cafe con Leche 是最流行的 XML 站点之一。他编写的图书包括 Effective XMLProcessing XML with JavaJava Network ProgrammingJava I/O。 目前,他在从事处理 XML 的 XOM API、Jaxen XPath 引擎和 Jester 测试覆盖工具的开发工作。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款