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.
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:
serialVersionUIDtransientmodifierreadObject()andwriteObject()writeReplace()andreadResolve()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.
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);
} |
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("", "");
} |
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.
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("", "");
} |
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);
} |
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.
Learn
- "Demystifying Extreme Programming: Test-driven programming"
(Roy Miller, developerWorks, April 2003): Explains what test-driven
programming is all about, and more importantly what it isn't
about.
- "
Keeping critters out of your code" (David Carew, Sandeep Desai,
Anthony Young-Garner, developerWorks, June 2003): Introduces unit
testing a server-side application server environment.
- "An early
look at JUnit 4" (Elliotte Rusty Harold, developerWorks, September
2005): Introduces the new annotation-based architecture of JUnit 4,
which requires Java 5 or later.
- Java I/O, Second
Edition (Elliotte Rusty Harold; O'Reilly, May 2006): The updated
edition discusses object serialization in depth.
-
Pragmatic Unit Testing (Dave Thomas and Andy Hunt; the
Pragmatic Programmer, September 2003): A complete introduction to unit
testing Java code.
- The Java technology
zone: Hundreds of articles about every aspect of Java
programming.
Get products and technologies
- JUnit: Get test infected.
Discuss
- developerWorks
blogs: Get involved in the developerWorks community.

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)





