In our Android app, which is partly written in Kotlin, we manage medical cases. The user can edit them. We validate changes by creating a clone in a presenter, change only the copy and compare it with the original. I used Parcelable to clone the case object as follows:
public Case cloneObject() {
Parcel parcel = null;
try {
parcel = Parcel.obtain();
parcel.writeParcelable(this, 0);
parcel.setDataPosition(0);
return parcel.readParcelable(Case.class.getClassLoader());
} finally {
if (parcel != null) {
parcel.recycle();
}
}
}
In the app we validate changes to the cases like this:
override fun validateHasChanges(): Boolean {
return !(model.updatedCase.title == model.originalCase.title &&
model.updatedCase.description == model.originalCase.description &&
model.updatedCase.category == model.originalCase.category &&
model.updatedCase.visibility == model.originalCase.visibility &&
areCaseFilesEqual() &&
model.updatedCase.recommendedFor == model.originalCase.recommendedFor &&
!model.wasConsentClicked &&
!model.wasImagesClicked)
}
Now I want to write a unit test and validate the correctness of the above. The problem is that Parcel has static and final methods and also a final void method, recycle(). I used PowerMockito to overcome most of this, but I cannot mock recycle anyhow, after spending days on it altogether. This is how it looks currently.
@RunWith(PowerMockRunner::class)
@PrepareForTest(Case::class, Parcel::class)
class QuickPostTest : BehaviorSpec() {
@Test
fun `validate has changes when category is different`() {
PowerMockito.mockStatic(Parcel::class.java)
val spy = PowerMockito.spy(Case())
val spy2 = PowerMockito.mock(Parcel::class.java)
PowerMockito.doNothing().`when`(spy2.recycle())
//suppress(method(Parcel::class.java, "recycle"))
Mockito.`when`(Parcel.obtain()).thenReturn(Whitebox.newInstance(Parcel::class.java))
Mockito.`when`(spy.cloneObject()).thenReturn(cloneObject(spy))
val originalCase = Case()
originalCase.title = "Cool"
val specialty = Specialty()
specialty.id = "anaesthetics"
val specialty2 = Specialty()
specialty2.id = "anaesthetics.anaesthesia.cardiothoracic"
originalCase.addSpecialty(specialty)
val model = BaseCreateCaseModel(originalCase, true)
model.updatedCase.addSpecialty(specialty2)
val presenter = CreateCaseQuickPresenter(originalCase)
presenter.setModel(model)
assertTrue(presenter.validateHasChanges())
}
private fun cloneObject(aCase: Case): Case {
var parcel: Parcel? = null
try {
parcel = Parcel.obtain()
parcel!!.writeParcelable(aCase, 0)
parcel.setDataPosition(0)
return parcel.readParcelable(Case::class.java.classLoader)
} finally {
if (parcel != null) {
parcel.recycle()
}
}
}
}
How should I modify it to make it work?
This is not the direct answer to my question, but I believe it's the right one. I chose Parcel to clone because I was convinced that it's the easiest solution, because Cloneable interface in Java is broken and copy constructor would be too complicated. But I was wrong. Adding Parcelable to my model introduced and Android dependency in my Presenter. Turns out, copy constructor in this case is not that complicated, even if my class hierarchy is very deep. So I did this, removed the spies and it works now. I had to keep PowerMockito, because I need Parcelable to pass around Case objects, but I don't need it in the Presenter, so mockStatic is enough.
Here is how the test looks like:
@RunWith(PowerMockRunner::class)
@PrepareForTest(Case::class, Parcel::class)
class QuickPostTest : BehaviorSpec() {
@Test
fun `validate has changes when category is different`() {
PowerMockito.mockStatic(Parcel::class.java)
val originalCase = Case()
originalCase.title = "Cool"
val specialty = Specialty()
specialty.id = "anaesthetics"
specialty.isActive = true
val specialty2 = Specialty()
specialty2.id = "anaesthetics.anaesthesia.cardiothoracic"
specialty2.isActive = true
originalCase.addSpecialty(specialty)
val model = BaseCreateCaseModel(originalCase, true)
model.updatedCase.addSpecialty(specialty2)
val presenter = CreateCaseQuickPresenter(originalCase)
presenter.setModel(model)
assertTrue(presenter.validateHasChanges())
}
}