Home > Net >  Error inflating ViewBinding in test class : Binary XML file line #38: Binary XML file line #38: Erro
Error inflating ViewBinding in test class : Binary XML file line #38: Binary XML file line #38: Erro

Time:12-16

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.

  • Related