【问题标题】:Error inflating ViewBinding in test class : Binary XML file line #38: Binary XML file line #38: Error inflating class <unknown>在测试类中膨胀 ViewBinding 时出错: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: TypedValue{t=0x2/d=0x7f04015d a=2}

我在测试类中找不到 ViewBinding inflate 的代码示例,这可能吗? I found this StackOverflow 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...
}

【问题讨论】:

    标签: android unit-testing mockk android-viewbinding androidx-test


    【解决方案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 编写解决方案
    猜你喜欢
    • 2017-06-05
    • 2017-01-13
    • 1970-01-01
    • 1970-01-01
    • 2013-04-25
    • 2020-06-27
    • 1970-01-01
    • 1970-01-01
    • 2011-05-29
    相关资源
    最近更新 更多