I am trying to write unit tests for a RecyclerView.ViewHolder class which uses ViewBinding but I am facing issues to inflate my ViewBinding in my test class, having this error when running my test :
Binary XML file line #38: Binary XML file line #38: Error inflating class <unknown> Caused by: java.lang.UnsupportedOperationException: Failed to resolve attribute at index 5: TypedValue{t=0x2/d=0x7f04015d a=2}
I could not find code examples of ViewBinding inflate in test classes, is that possible ? I found this StackOverflow thread but it uses PowerMock to mock a ViewBinding class. I'm using mockK in my project and I think using a real ViewBinding instance would be better in my case.
My ViewHolder looks like this :
class MemoViewHolder(private val binding: MemoItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(data: Memo) {
with(binding) {
// doing binding with rules I would like to test
}
}
}
My test class looks like this. I am using MockK and Robolectric to get application context
@RunWith(RobolectricTestRunner::class)
class MemoViewHolderTest {
private lateinit var context: PostFilesApplication
@Before
fun setUp() {
MockKAnnotations.init(this)
context = ApplicationProvider.getApplicationContext()
}
@Test
fun testSuccess() {
val viewGroup = mockk<ViewGroup>(relaxed = true)
val binding = MemoItemBinding.inflate(LayoutInflater.from(context), viewGroup, false)
}
}
CodePudding user response:
I was able to get this working (using Mockito, but it should be applicable to MockK too) by looking in the generated binding class to see what methods I needed to mock to get it to inflate and return mocked views properly. These files are in app/build/generated/data_binding_base_class_source_out/debug/out/your/package/databinding
for a standard build
Here is an example of a generated data binding class with three views in a ConstraintLayout.
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
@NonNull
public final Button getText;
@NonNull
public final ProgressBar progress;
@NonNull
public final TextView text;
private ActivityMainBinding(@NonNull ConstraintLayout rootView, @NonNull Button getText,
@NonNull ProgressBar progress, @NonNull TextView text) {
this.rootView = rootView;
this.getText = getText;
this.progress = progress;
this.text = text;
}
@Override
@NonNull
public ConstraintLayout getRoot() {
return rootView;
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_main, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public static ActivityMainBinding bind(@NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
int id;
missingId: {
id = R.id.get_text;
Button getText = ViewBindings.findChildViewById(rootView, id);
if (getText == null) {
break missingId;
}
id = R.id.progress;
ProgressBar progress = ViewBindings.findChildViewById(rootView, id);
if (progress == null) {
break missingId;
}
id = R.id.text;
TextView text = ViewBindings.findChildViewById(rootView, id);
if (text == null) {
break missingId;
}
return new ActivityMainBinding((ConstraintLayout) rootView, getText, progress, text);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
To be able to call inflate and have the binding holding mocked views in the unit test, you need to mock several sets of calls
@Before
fun setUp() {
// return the mock root from the mock inflater
doReturn(mMockConvertView).`when`(mMockInflater).inflate(R.layout.my_layout, mMockViewGroup, false)
// extra mocks to handle findChildViewById
doReturn(1).`when`(mMockConvertView).childCount
doReturn(mMockConvertView).`when`(mMockConvertView).getChildAt(0)
// Return the mocked views
doReturn(mMockText).`when`(mMockConvertView).findViewById<View>(R.id.text)
doReturn(mMockButton).`when`(mMockConvertView).findViewById<View>(R.id.get_text)
doReturn(mMockProgBar).`when`(mMockConvertView).findViewById<View>(R.id.progress)
}
They recently changed it to use ViewBindings.findChildViewById
instead of just findViewById
, which required extra mocking.
@Nullable
public static <T extends View> T findChildViewById(View rootView, @IdRes int id) {
if (!(rootView instanceof ViewGroup)) {
return null;
}
final ViewGroup rootViewGroup = (ViewGroup) rootView;
final int childCount = rootViewGroup.getChildCount();
for (int i = 0; i < childCount; i ) {
final T view = rootViewGroup.getChildAt(i).findViewById(id);
if (view != null) {
return view;
}
}
return null;
}
Keep in mind that they may change the structure of the auto-generated code in the future, which will break unit tests like this. This happened recently, when they switched to this static method, and it would not surprise me if it happened again in the future.
With these defined, then you could call
val binding = ActivityMainBinding.inflate(mMockInflater, mMockViewGroup, false)
to get an actual binding instance holding your mocked views.