Skip to main content

Testing object serialization

An important test that's easy to forget

Elliotte Rusty Harold (elharo@metalab.unc.edu), Adjunct Professor, Polytechnic University
Photo of Elliotte Rusty Harold
Elliotte Rusty Harold is originally from New Orleans, to which he returns periodically in search of a decent bowl of gumbo. However, he resides in the Prospect Heights neighborhood of Brooklyn with his wife Beth and cats Charm (named after the quark) and Marjorie (named after his mother-in-law). He's an adjunct professor of computer science at Polytechnic University, where he teaches Java and object-oriented programming. His Cafe au Lait Web site has become one of the most popular independent Java sites on the Internet, and his spin-off site, Cafe con Leche, has become one of the most popular XML sites. His books include Effective XML, Processing XML with Java, Java Network Programming, and Java I/O. He's currently working on the XOM API for processing XML, the Jaxen XPath engine, and the Jester test coverage tool.

Summary:  Even great developers sometimes forget to test object serialization, but that doesn't excuse you from making the same mistake. In this article, Elliotte Rusty Harold explains the importance of unit testing object serialization and leaves you with some tests to remember.

Date:  13 Jun 2006
Level:  Introductory
Activity:  5899 views

A general principle of test-driven development is that you should test all published interfaces to a class. If a client can call a method or access a field, test it. However, many classes in the Java™ language have a published interface that's easy to forget: the serialized objects generated from the class's instances. Sometimes these classes implement Serializable explicitly. Sometimes they merely inherit it from a superclass. In either case, you should test their serialized forms. In this article I demonstrate the various ways to test object serialization.

Testing serialization

Serialization is especially important to test because it is very, very easy to get wrong. It's particularly easy to break all existing serialized objects when fixing a bug or optimizing a class. If you don't think about serialization while changing code, it's virtually guaranteed that you will break the legacy objects. This is a critical bug if you're using serialization for any form of persistent storage. Even if you're only using object serialization for transient message passing between processes as in RMI, changing the serialization format prevents systems that don't have exactly the same versions of each class from exchanging data.

Fortunately, you can usually avoid incompatible changes when working on a class if you're careful and pay attention to serialization issues. The Java language provides numerous means to maintain compatibility between different versions of a class, including the following:

  • serialVersionUID
  • transient modifier
  • readObject() and writeObject()
  • writeReplace() and readResolve()
  • serialPersistentFields

The biggest problem with these solutions is that programmers don't use them. When you're focused on fixing a bug, adding a feature, or curing a performance problem, you usually don't stop to think about the effect your changes will have on serialization. However serialization is a wide-ranging concern that cuts across many different layers of a system. Almost any change you make that involves a class's instance fields has some impact on serialization. This is where unit testing comes to the rescue. In the sections that follow, I show you a few simple unit tests that ensure that you do not inadvertently change the serialization format of your serializable classes.


Can I serialize this?

The first serialization test you usually write is one that verifies serialization is possible. Even if a class implements Serializable, there's no guarantee that it can be serialized. For instance, if a serializable container such as an ArrayList contains a non-serializable object such as a Socket, it throws a NotSerializableException when you try to serialize it.

Usually for this test, you just write the data onto a ByteArrayOutputStream. If no exception is thrown, the test passes. If you like, you can also test that some output has been written. For example, the code fragment in Listing 1 tests whether the BaseXPath class from Jaxen is serializable:


Listing 1. Is the class serializable?
  
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);

    }


Testing the serialized form

Next, you want to write a test that verifies not only that the output is present, but that it's correct. There are two ways you can do this:

  • Deserialize the object and compare it to the original.
  • Compare it byte-per-byte to a reference .ser file.

I usually start with the first option because it also provides a simple test for deserialization, and it's a little easier to code and implement. For example, the code fragment in Listing 2 tests whether the SimpleVariableContext class from Jaxen can be written and then read back in:


Listing 2. Deserialize the object and compare it to the original
  
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("", "");

  }

Let's try that one again...

You almost always find bugs when testing parts of a code base that have never been tested before, and object serialization is no different. The first time I ran the test in Listing 2 it failed, as you can see from the output in Listing 3:


Listing 3. Not serializable
 
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)

It turned out that SimpleVariableContext contained a reference to a QualifiedName object, and the QualifiedName class was not marked Serializable. I added implements Serializable to the class signature of QualifiedName and the test then passed.

Note that this test doesn't actually verify that the serialization format is correct -- only that it's round-tripable. To test for correctness, you need to generate some reference files that you can compare to the output from all future versions of the class.


Testing deserialization

You usually can't rely on the default serialization format to retain file-format compatibility between different versions of a class. You have to customize it in a variety of ways using serialPersistentFields, readObject() and writeObject() methods, and/or the transient modifier. If you do make an incompatible change to the serialization format of a class, you should also change the serialVersionUID field to indicate that you've done so.

Normally you don't care much about the detailed structure of serialized objects. You just care that whatever format you start out with is maintained as the class evolves. Once the class is in more-or-less complete shape, write some serialized instances of the class and store them where you can use them as references going forward. (You probably do want to think at least a little about how you will serialize to ensure sufficient flexibility for evolution going forward.)

The program that writes the serialized instances is throwaway code. You won't need it more than once. In fact, you specifically shouldn't run it more than once because you don't want to pick up any accidental changes in the serialization format. For example, Listing 4 shows a program I used to serialize the SimpleVariableContext class from Jaxen:


Listing 4. A program for writing serialized instances
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();

  }

}

All you need to do is write a serialized object into a file, and you only do that once. It's the file you want to save, not the code that wrote it. Your test deserializes the object in the file and then compare its properties to their expected values. For example, Listing 5 shows the compatibility test for Jaxen's SimpleVariableContext class:


Listing 5. Ensure that the file format has not changed
  
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("", "");

  }


Testing non-serializability

Classes are often serializable by default. For example, any subclass of java.lang.Throwable or java.awt.Component inherits serializability from its ancestor. This is sometimes what you want, but not always. In some cases, serialization can be a security hole, enabling rogue programmers to create objects without calling the constructors or setter methods, and thus bypassing any constraints checking you've carefully built into your class.

If you want the class to be serializable, you'll need to test it, just as you would test a class that directly implements Serializable. If you don't want the class to be serializable, then you should override writeObject() and readObject() so that both throw a NotSerializableException, and then you'll need to test that.

Such a test is implemented like any other JUnit exception test. Simply wrap a try block around the statements that should throw the exception and then add a fail() statement right after the statement that's supposed to throw the exception. If you like, you can assert something about the thrown exception in the catch block. For example, Listing 6 checks that FunctionContext is not serializable:


Listing 6. Test that a FunctionContext is not serializable
  
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 and JUnit 4 make exception testing even easier. Just declare the expected exception in the @Test annotation, as shown in Listing 7:


Listing 7. Exception testing with annotations

@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);

  }


In conclusion

Serialization formats can be the most fragile and least robust parts of a code base. It sometimes feels like you can break them just by looking at them funny. Unit testing and test-driven development are excellent tools for managing such fragile systems with confidence, but they only work if you use them.

If you care about object serialization, and especially if you intend to use serialized objects for long-term persistent storage, you must test serialization. Do not assume your Java code will just do the right thing -- it probably won't! If you make serialization tests a regular part of your test suite, however, then maintaining long-term compatibility becomes relatively straightforward. The time you spend unit testing object serialization now will be paid back manyfold in the time you save on debugging later.


Resources

Learn

Get products and technologies

  • JUnit: Get test infected.

Discuss

About the author

Photo of Elliotte Rusty Harold

Elliotte Rusty Harold is originally from New Orleans, to which he returns periodically in search of a decent bowl of gumbo. However, he resides in the Prospect Heights neighborhood of Brooklyn with his wife Beth and cats Charm (named after the quark) and Marjorie (named after his mother-in-law). He's an adjunct professor of computer science at Polytechnic University, where he teaches Java and object-oriented programming. His Cafe au Lait Web site has become one of the most popular independent Java sites on the Internet, and his spin-off site, Cafe con Leche, has become one of the most popular XML sites. His books include Effective XML, Processing XML with Java, Java Network Programming, and Java I/O. He's currently working on the XOM API for processing XML, the Jaxen XPath engine, and the Jester test coverage tool.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology, Open source
ArticleID=127393
ArticleTitle=Testing object serialization
publish-date=06132006
author1-email=elharo@metalab.unc.edu
author1-email-cc=dwxed@us.ibm.com

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers