Testing Parcelable and Serializable classes

It’s hard to write an app in Android without coming across the need to serialise data using either Parcelable or Serializable. In this article, we will explore why at Babylon Health we test our implementations of these interfaces even when using code-generation tools like @Parcelize, and the tools we use to help us write tests that work for all our classes.

Why test Parcelable and Serializable implementations

Our codebase contains over 50 Parcelable and Serializable objects. When I started at Babylon, most of these were implemented using AutoParcel; however, due to build performance issues using annotation processors, I re-wrote most of these by hand.

It is worth noting that at the time @Parcelize was still experimental, so we chose not to use it. Of course, this is now officially released, and our codebase has since evolved to use it.

Aside from writing less code, another great advantage of using @Parcelize is that if an object cannot be parcelled the IDE complains. Thus, reducing the chances of your app crashing in production.

Unfortunately, not all scenarios are spotted, for example, consider the following code where a Parcelable contains a Serializable:

@Parcelize
data class Parcel(val serial: Serial) : Parcelable

data class Serial(val notSerial: NotSerial) : Serializable

data class NotSerial(val id: String)

Parcelling throws an IOException at runtime because NotSerial does not implement Serializable and there is no compile-time protection for this. We don’t currently enforce that a Parcelable shouldn’t contain Serializable objects, so the above is always a risk.

Additionally, there are times when you write your own Parceler, for example, for external classes in conjunction with @TypeParceler or @WriteWith, and as it’s a manual process, there’s a risk of making a mistake with these as well.

For these reasons, we feel it is good to have tests in place on our Parcelable and Serializable implementations.

Creating the test

Principally the process of testing a Parcelable involves creating an instance of the class under test, parcel it, un-parcel it and verify the result matches the original.

We don’t want to write separate unit tests for each individual Parcelable, as the bulk of the code should be near identical. Instead, it makes sense to create a parameterised test, but how do you find every class that implements Parcelable?

Finding all Parcelables

A little known tool we use to find all classes that implement a particular interface is ClassGraph.

This tool scans the JVM classpath and allows you to, for example, “find all classes that implement a given interface”.

Firstly, we scan the classpath, which, as we are only interested in our classes, we filter by package using whitelistPackages:

val classgraph = ClassGraph()
.enableAllInfo()
.whitelistPackages("com.babylon")
.scan()

With the results of the scan, we can then find all classes implementing Parcelable. We are only interested in concrete implementations and so filter out interfaces, abstract classes or generic classes before loading the classes:

fun ClassInfo.hasTypeParams() =
typeSignature?.typeParameters?.size ?: 0 > 0

val parcelables =
classgraph.getClassesImplementing("android.os.Parcelable")
.filter {
!(it.isInterface || it.isAbstract || it.hasTypeParams())
}
.loadClasses()

The result of loadClasses is List<Class<*>>, the next step is to instantiate each of these into real objects.

Creating real objects

Instantiating real objects from a Class isn’t that straight forward, the interface may provide a newInstance function, but that can only instantiate classes with a default zero-parameter constructor.

Alternatively, you could use the reflection API to query the classes constructor and attempt to pass in default values for each parameter. But, why do this yourself when a tool called JFixture will do it for you.

Types are generated based on the concept of “constrained non-determinism”. From a practical point of view, this means random values are used rather than the same fixed value giving the advantage of helping write “tests by specification”.

Creating an instance of a class using JFixture is pretty straightforward, the one caveat to look out for though is if the Class represents an object class then you need to retrieve it with currentClass.kotlin.objectInstance:

val fixture = JFixture()
val
objectInstance = currentClass.kotlin.objectInstance ?:
fixture.create<Parcelable>(currentClass)

Serialising and deserialising Parcelables

So we now have a concrete implementation of our Parcelable as objectInstance containing unknown data. To verify the Parcelable works, we need to serialise, deserialise and check that the results match the input.

Serialisation works by simply writing to a Parcel and marshalling it to a byte array:

val serializedBytes = Parcel.obtain().run {
writeParcelable(objectInstance, 0)
marshall()
}

Deserialisation is just as straightforward where you un-marshall the bytes and read the Parcelable:

val result = Parcel.obtain().run {
unmarshall(serializedBytes, 0, serializedBytes.size)
setDataPosition(0)
readParcelable<Parcelable>(this::class.java.classLoader)
}

Similarly, if you were to test Serializable implementations, the following code could be used:

val serializedBytes = ByteArrayOutputStream().use {
byteArrayOutputStream ->
ObjectOutputStream(byteArrayOutputStream).use {
it
.writeObject(objectInstance)
}
byteArrayOutputStream.toByteArray()
}

val
result = ByteArrayInputStream(serializedBytes).use {
ObjectInputStream(it).use {
it
.readObject()
}
}

Given you have serialised and deserialised the object, the next challenge is to compare the content of the two objects.

Comparing objects

The biggest challenge with comparing our original object and the result is our Parcelable can contain anything from simple primitive parameters to deeply nested objects. Fortunately, Shazamcrest provides Hamcrest matchers that allow assertions on complete “beans” through sameBeanAs. Internally the library works by using GSON to serialise the objects to JSON before comparing.

Using Shazamcrest means we can then compare our objects with one line of code:

assertThat(result, sameBeanAs(objectInstance))

There is one catch we saw with this approach; consider the following code:

sealed class Sealed(
open val id
: String
) : Parcelable {

@Parcelize
data class DataClass(
override val id
: String
) : Sealed(id)
}

Decompiling this code results in the following code which we can see contains two id fields:

public abstract class Sealed implements Parcelable {
@NotNull
private final String id;

@NotNull
public String getId() {
return this
.id;
}



@Parcelize
public static final class DataClass extends Sealed {
@NotNull
private final String id;

@NotNull
public String getId() {
return this
.id;
}


}
}

Shazamcrest doesn’t like these duplicate id fields. The fix in this example is relatively simple, make the id field abstract in the sealed class, so it has no backing field.

Putting it all together

Using ClassGraph we find our Parcelable classes, JFixture to generate instances, and finally, Shazamcrest to verify the results. The remaining task is turning this into a test.

To read and write Parcelable, we need the Android framework, so to write this as a unit test rather than instrumentation, we need to use Robolectric.

Robolectric handily provides ParameterizedRobolectricTestRunner for writing parameterised tests, which makes our test code look like this:

private val scanResult: List<Array<Any>> by lazy {
val
classLoader = Thread.currentThread().contextClassLoader
ClassGraph()
// Use contextClassLoader to avoid ClassCastExceptions
.addClassLoader(classLoader)
.enableAllInfo()
.blacklistPackages("android")
.whitelistPackages("com.babylon")
.scan()
.getClassesImplementing("android.os.Parcelable")
.filter { !(it.isInterface || it.isAbstract || it.typeSignature?.typeParameters?.size ?: 0 > 0) }
.loadClasses()
.map { arrayOf<Any>(it.name, it) }
}

@RunWith(ParameterizedRobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class
ParcelableTest(val name: String, private val currentClass: Class<Parcelable>) {

private lateinit var objectInstance
: Parcelable

@Before
fun setUp() {
val
fixture = JFixture()
objectInstance
= currentClass.kotlin.objectInstance ?: fixture.create<Parcelable>(currentClass)
}

@Test
fun testParcelable() {
// serialise
val serializedBytes = Parcel.obtain().run {
writeParcelable(objectInstance, 0)
marshall()
}

// ensure there are some bytes
assertNotNull(serializedBytes)

// deserialize
val result = Parcel.obtain().run {
unmarshall(serializedBytes, 0, serializedBytes.size)
setDataPosition(0)
readParcelable<Parcelable>(this::class.java.classLoader)
}

// ensure object created matches the original
assertThat(result, sameBeanAs(objectInstance))
}

companion object {
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun
provideObjects(): List<Array<Any>> = scanResult
}
}

Closing notes

Hopefully, we have demonstrated some of the power of ClassGraph, but it is not without its problems. It takes time to scan the classpath, plus ClassGraph uses a lot of memory, which can cause some instability in your test suite.

On the positive side though, the test shown in this article allowed us to spot two classes that would have crashed our app in production and is helping prevent future similar incidents without us having to write individual tests for every Parcelable or Serializable.

Matt Dolan works as an Android Engineer at Babylon Health. If you would like to join him in building the future of healthcare apply here.

Matt Dolan has been eating doughnuts and developing with Android since the dark days of v1.6.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store