 | Level: Introductory Elliotte Harold (elharo@metalab.unc.edu), Adjunct Professor, Polytechnic University
13 Jun 2006 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.
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
- "Incremental development with Ant and JUnit" (Malcolm Davis,
developerWorks, November 2000): Introduces unit testing on the Java
platform.
- "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
About the author  | 
|  | 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. |
Rate this page
|  |