Kevin Schultz

Mobile Engineering

How (Not) to Test Android's Parcelable Interface

| Comments

Android applications invariably seem to have many places where you have to pack data into an Intent or Bundle in order to pass it on to another component. I have found that packing a single Java object in the payload is far more maintainable than using several primitive key/value pairs. If you decide to add or remove an a field to the object later, you won’t have to go back and modify every single Intent & Bundle. In order to do this, the object must implement either Serializable or Parcelable. I generally prefer Parcelable, even though it takes a bit more code to implement.

Unfortunately I ran into a huge pitfall with Parcelable objects. I have been using a very basic unit test to ensure my Parcelable implementation was correct, and didn’t realize the test wasn’t actually proving everything was working as intended. The other day I was refactoring some code and seeing ClassCastExceptions when pulling a particular Parcelable object out of a Bundle. I couldn’t figure out where the type was getting switched in my code, but it never occurred to me that the actual Parcelable implementation was at fault because I had too much faith in my unit tests. After a couple hours of digging I figured out that the naive unit tests I have been using for Parcelable don’t actually ensure anything. Hopefully this post can help you avoid the same mistake.

First, a quick summary of implementing Parcelable if you have not done it before. In order to make an object Parcelable, you must handle writing to the Parcel, creating an object from the Parcel, and provide a CREATOR factory. This code is pretty straightforward although there are a few gotchas. Take a look at the implementation below for class ‘Foo’. Note that in order to test these types of classes I always make sure to write (or generate) a proper equals() method.

Complete (Correct) Parcelable Implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class Foo implements Parcelable {
    private long mId;
    private int mCount;
    private String mName;
    private boolean mFlag;

    /** Generic constructor */
    public Foo(long id, int count, String name, boolean flag) {
        this.mId = id; this.mCount = count; this.mName = name; this.mFlag = flag;
    }

    /** Parcelable constructor */
    private Foo(Parcel in) {
        this.mId = in.readLong();
        this.mCount = in.readInt();
        this.mName = in.readString();
        // Again, there is no readBoolean() method, so use an int or byte instead
        this.mFlag = in.readInt() == 1;
    }
    // Note that order of writing variables to parcel 
    // must exactly match order in parcelable constructor 
    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeLong(mId);
        out.writeInt(mCount);
        out.writeString(mName);
        // How hard would it have been to include a writeBoolean method? Come on
        out.writeInt(mFlag ? 1 : 0);
    }
    public static final Parcelable.Creator<Foo> CREATOR = new Parcelable.Creator<Foo>() {
        public Foo createFromParcel(Parcel in) {
            return new Foo(in);
        }

        public Foo[] newArray(int size) {
            return new Foo[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Foo)) {
            return false;
        }
        Foo foo = (Foo) o;
        if (mCount != foo.mCount || mFlag != foo.mFlag || mId != foo.mId) {
            return false;
        }
        if (!TextUtils.equals(mName, foo.mName)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int result = (int) (mId ^ (mId >>> 32));
        result = 31 * result + mCount;
        result = 31 * result + (mName != null ? mName.hashCode() : 0);
        result = 31 * result + (mFlag ? 1 : 0);
        return result;
    }
}

(Bonus rant: why did the Android team not include writeBoolean() and readBoolean() methods? I generally pack booleans as an int but you can also use a byte or a String to pass them. Thanks to this answer on StackOverflow for the inspiration on how to pass them cleanly.)

My original unit test naively mirrored how you actually use Parcelable objects. Stick it in a Bundle, pull it out of a Bundle, and check equality.

Incorrect Parcelable Unit Test
1
2
3
4
5
6
7
8
    // This doesn't actually test anything
    public void testParcelableInterface() {
        Foo foo = new Foo(1L, 3, "name", false);
        Bundle bundle = new Bundle();
        bundle.putParcelable("foo", foo);
        Foo parceledFoo = bundle.getParcelable("foo");
        assertEquals(foo, parceledFoo);
    }

Thus, the above test passed even though the Foo class’s CREATOR was written originally as shown below. Note that CREATOR is returning type Bar instead of type Foo. This is one easy pitfall when writing the boilerplate associated with Parcelable.

Incorrect Parcelable CREATOR type
1
2
3
4
5
6
7
8
    public static final Parcelable.Creator<Bar> CREATOR = new Parcelable.Creator<Bar>() {
        public Bar createFromParcel(Parcel in) {
            return new Bar(in);
        }
        public Bar[] newArray(int size) {
            return new Bar[size];
        }
    };

After finding the actual bug, my concern switched to figuring out why the unit tests hadn’t caught it. It turns out that Bundle doesn’t actually serialize/de-serialize each value until the Bundle itself is parceled. Thankfully James Wilson has a nice solution that correctly tests the object.

Correct Parcelable Unit Test
1
2
3
4
5
6
7
8
    public void testParcelable() {
        Foo foo = new Foo(1L, 3, "name", false);
        Parcel parcel = Parcel.obtain();
        foo.writeToParcel(parcel, 0);
        parcel.setDataPosition(0);
        Foo parceledFoo = Foo.CREATOR.createFromParcel(parcel);
        assertEquals(foo, parceledFoo);
    }

In fact, with this code the test suite won’t even build because CREATOR’s return value is type checked at compile time, rather than casting from Bundle’s getParcelable() at runtime.

I would recommend writing that unit test anytime you are creating a Parcelable object.

Comments