garbagecollector
garbagecollector

Reputation: 3880

Android Unit Testing: Bundle/Parcelable

How do you unit test Parcelable? I created a Parcelable class, and wrote this unit test

TestClass test = new TestClass();
Bundle bundle = new Bundle();
bundle.putParcelable("test", test);

TestClass testAfter = bundle.getParcelable("test");
assertEquals(testAfter.getStuff(), event1.getStuff());

I purposely try to fail the test by returning null in the createFromParcel(), but it seems to succeed. It looks like it doesn't get parceled until it's needed. How do I force the Bundle to..bundle?

Upvotes: 43

Views: 21620

Answers (4)

Stéphane
Stéphane

Reputation: 1779

I'm using a parcel helper class to convert an object to/from parcel.

See this gist: https://gist.github.com/scallacs/f749a7385bcf75829a98d7b651efd02e

Usage is very simple:

Model model = new Model("HelloWorld");
// Create an object from the parcel model
Model createdFromParcel = ParcelTestHelper.createFromParcel(model, model.CREATOR);

// Do your assertions...
assertEquals(model.value, createdFromParcel.value);

The implementation:

import android.os.Parcel;
import android.os.Parcelable;

public class ParcelTestHelper {

    public static String DEFAULT_CREATOR_FIELD = "CREATOR";

    public static <T extends Parcelable> T createFromParcel(T input, Parcelable.Creator<T> creator) {
        Parcel parcel = toParcel(input);
        return fromParcel(parcel, creator);
    }

    public static <T extends Parcelable> T createFromParcel(T input) throws NoSuchFieldException, IllegalAccessException {
        return createFromParcel(input, getCreator(input));
    }

    public static <T extends Parcelable> Parcel toParcel(T input) {
        Parcel parcel = Parcel.obtain();
        input.writeToParcel(parcel, input.describeContents());
        parcel.setDataPosition(0);
        return parcel;
    }

    public static <T> Parcelable.Creator<T> getCreator(T input) throws NoSuchFieldException, IllegalAccessException {
        return getCreator(input, DEFAULT_CREATOR_FIELD);
    }

    public static <T> Parcelable.Creator<T> getCreator(T input, String field) throws NoSuchFieldException, IllegalAccessException {
        Object creator = input.getClass().getField(field).get(input);
        if (!(creator instanceof Parcelable.Creator)) {
            throw new InternalError("Should have field " + field + " instance of Parcelable.Creator");
        }
        return (Parcelable.Creator<T>) creator;
    }

    public static <T extends Parcelable> T fromParcel(Parcel parcel, Parcelable.Creator<T> creator) {
        return creator.createFromParcel(parcel);
    }
}

Upvotes: 0

dewey
dewey

Reputation: 312

Since this question and answers helped me several years down the line, I thought I'd add my own suggestion, which would be to assert that the dataPosition() at the end of the read was the same as at the end of the write. Building on Xilconic's answer:

@Test
public void testTestClassParcelable(){
    TestClass test = new TestClass();

    // Obtain a Parcel object and write the parcelable object to it:
    Parcel parcel = Parcel.obtain();
    test.writeToParcel(parcel, 0);

    //>>>>> Record dataPosition
    int eop = parcel.dataPosition();

    // After you're done with writing, you need to reset the parcel for reading:
    parcel.setDataPosition(0);

    // Reconstruct object from parcel and asserts:
    TestClass createdFromParcel = TestClass.CREATOR.createFromParcel(parcel);
    assertEquals(test, createdFromParcel);

    //>>>>> Verify dataPosition
    assertEquals(eop, parcel.dataPosition());
}

Background: This came to me after spending an (embarrassing) amount of time debugging a bad Parcelable. In my case, writeToParcel was writing a duplicate field from one object within a moderately complex object graph. Therefore subsequent objects were read incorrectly, giving misleading exceptions, and no errors on tests with the specific misbehaving object.

It was a pain to track down, and then I realized checking dataPosition would have pinpointed the problem faster since I have tests on inner objects as well as the main container.


Kotlin: Since I am working in Kotlin, a little lambda and reifying magic:

class ParcelWrap<T>(val value: T)

val <T> T.parcel: ParcelWrap<T> get() = ParcelWrap(this)

inline fun <reified T: Parcelable> ParcelWrap<T>.test(
        flags: Int = 0,
        classLoader: ClassLoader = T::class.java.classLoader,
        checks: (T) -> Unit
): T {
    // Create the parcel
    val parcel: Parcel = Parcel.obtain()
    parcel.writeParcelable(this.value, flags)

    // Record dataPosition
    val eop = parcel.dataPosition()

    // Reset the parcel
    parcel.setDataPosition(0)

    // Read from the parcel
    val newObject = parcel.readParcelable<T>(classLoader)

    // Perform the checks provided in the lambda
    checks(newObject)

    // Verify dataPosition
    assertEquals("writeToParcel wrote too much data or read didn't finish reading", eop, parcel.dataPosition())
    return newObject
}

I can now test very easily along these lines, if there is a complete and reliable equals():

testObject.parcel.test { assertEquals(testObject, it) }

Note the .parcel.test avoids having to re-specify generic type parameter using this answer.

Or, for more complex assertions:

testObject.parcel.test {
    assertEquals(123, it.id)
    assertEquals("dewey", it.name)
    // ...
}

Upvotes: 7

Xilconic
Xilconic

Reputation: 3845

I've found this link showing how you can unit test a parcelable object: http://stuffikeepforgettinghowtodo.blogspot.nl/2009/02/unit-test-your-custom-parcelable.html

You can actually skip the Bundle if you don't really need to include it like zorch did his suggestion. You would then get something like this:

public void testTestClassParcelable(){
    TestClass test = new TestClass();

    // Obtain a Parcel object and write the parcelable object to it:
    Parcel parcel = Parcel.obtain();
    test.writeToParcel(parcel, 0);

    // After you're done with writing, you need to reset the parcel for reading:
    parcel.setDataPosition(0);

    // Reconstruct object from parcel and asserts:
    TestClass createdFromParcel = TestClass.CREATOR.createFromParcel(parcel);
    assertEquals(test, createdFromParcel);
}

Upvotes: 102

chivorotkiv
chivorotkiv

Reputation: 745

You can do it by this way:

//Create parcelable object and put to Bundle
    Question q = new Question(questionId, surveyServerId, title, type, answers);
    Bundle b = new Bundle();
    b.putParcelable("someTag", q);

//Save bundle to parcel
    Parcel parcel = Parcel.obtain();
    b.writeToParcel(parcel, 0);

//Extract bundle from parcel
    parcel.setDataPosition(0);
    Bundle b2 = parcel.readBundle();
    b2.setClassLoader(Question.class.getClassLoader());
    Question q2 = b2.getParcelable("someTag");

//Check that objects are not same and test that objects are equal
    assertFalse("Bundle is the same", b2 == b);
    assertFalse("Question is the same", q2 == q);
    assertTrue("Questions aren't equal", q2.equals(q));

Upvotes: 19

Related Questions