在测试类中膨胀 ViewBinding 时出错:Binary XML file line #38: Binary XML file line #38: Error inflating class &l

Posted

技术标签:

【中文标题】在测试类中膨胀 ViewBinding 时出错:Binary XML file line #38: Binary XML file line #38: Error inflating class <unknown>【英文标题】:Error inflating ViewBinding in test class : Binary XML file line #38: Binary XML file line #38: Error inflating class <unknown> 【发布时间】:2022-01-18 15:57:10 【问题描述】:

我正在尝试为使用 ViewBinding 的 RecyclerView.ViewHolder 类编写单元测试,但我在测试类中膨胀 ViewBinding 时遇到问题,运行测试时出现此错误: Binary XML file line #38: Binary XML file line #38: Error inflating class &lt;unknown&gt; Caused by: java.lang.UnsupportedOperationException: Failed to resolve attribute at index 5: TypedValuet=0x2/d=0x7f04015d a=2

我在测试类中找不到 ViewBinding inflate 的代码示例,这可能吗? I found this *** thread 但它使用 PowerMock 来模拟 ViewBinding 类。我在我的项目中使用 mockK,我认为在我的情况下使用真正的 ViewBinding 实例会更好。

我的 ViewHolder 看起来像这样:

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
        
    

我的测试类看起来像这样。我正在使用 MockKRobolectric 来获取应用程序上下文

@RunWith(RobolectricTestRunner::class)
class MemoViewHolderTest 

    private lateinit var context: MyApplication

    @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)
    

编辑: 这是来自@tyler-v 的答案的 mockK 版本

@RelaxedMockK
private lateinit var layoutInflater: LayoutInflater
@RelaxedMockK
private lateinit var rootView: ConstraintLayout // must be the type of the root view in the layout
@RelaxedMockK
private lateinit var groupView: ViewGroup
// mock every views in your layout
@RelaxedMockK
private lateinit var title: TextView

@Before
fun setUp() 
    context = ContextThemeWrapper(
        ApplicationProvider.getApplicationContext<MyApplication>(),
        R.style.AppTheme
    )
    MockKAnnotations.init(this)
    every  layoutInflater.inflate(R.layout.memo_item, groupView, false)  returns rootView
    every  rootView.childCount  returns 1
    every  rootView.getChildAt(0)  returns rootView
    // mock findViewById for each view in the memo_item layout
    every  rootView.findViewById<TextView>(R.id.title)  returns title


@After
fun tearDown() 
    unmockkAll()


@Test
fun testBindUser() 
    val binding = MemoItemBinding.inflate(layoutInflater, groupView, false)
    MemoListAdapter.MemoViewHolder(binding).bind(memoList[0])
    // some tests...

【问题讨论】:

【参考方案1】:

通过查看生成的绑定类以查看我需要哪些方法,我能够让这个工作(使用 Mockito,但它也应该适用于 MockK)模拟以使其膨胀并正确返回模拟视图。这些文件位于app/build/generated/data_binding_base_class_source_out/debug/out/your/package/databinding 中,用于标准构建

这是一个在 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));
  

为了能够在单元测试中调用 inflate 并让绑定持有模拟视图,您需要模拟几组调用

@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)

他们最近将其更改为使用 ViewBindings.findChildViewById 而不仅仅是 findViewById,这需要额外的模拟。

@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;

请记住,它们将来可能会更改自动生成代码的结构,这会破坏这样的单元测试。最近发生了这种情况,当时他们切换到了这种静态方法,如果以后再发生这种情况,我也不会感到惊讶。

定义了这些,就可以调用

val binding = ActivityMainBinding.inflate(mMockInflater, mMockViewGroup, false)

获取一个实际的绑定实例来保存您的模拟视图。

【讨论】:

太好了,终于成功了!感谢您的帮助,我确实阅读了生成的绑定类,但我没有像您那样挖掘足够的东西来找到正确的模拟。我将编辑我的原始帖子以使用 mockK 编写解决方案

以上是关于在测试类中膨胀 ViewBinding 时出错:Binary XML file line #38: Binary XML file line #38: Error inflating class &l的主要内容,如果未能解决你的问题,请参考以下文章

第 7 行的 InflateException 二进制 XML 文件:膨胀类片段时出错

如何解决:二进制 XML 文件第 8 行:膨胀类时出错 [重复]

二进制 XML 文件第 0 行:使用 api 26 膨胀类 TextView 时出错

膨胀 android.support.v17.leanback.widget.TitleView 时出错

使用导航膨胀布局时出错

在 RecyclerView 上膨胀类 ImageView 时出错