在测试 class 中膨胀 ViewBinding 时出错:二进制 XML 文件行 #38:二进制 XML 文件行 #38:膨胀 class <unknown> 时出错

Error inflating ViewBinding in test class : Binary XML file line #38: Binary XML file line #38: Error inflating class <unknown>

我正在尝试为使用 ViewBinding 的 RecyclerView.ViewHolder class 编写单元测试,但我在测试 class 时遇到了膨胀 ViewBinding 的问题,当 运行 我的测试: 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}

我在测试 classes 中找不到 ViewBinding inflate 的代码示例,这可能吗? I found this Whosebug thread 但它使用 PowerMock 模拟 ViewBinding class。我在我的项目中使用 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
        }
    }
}

我的测试 class 看起来像这样。我正在使用 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...
}

我能够通过查看生成的文件来实现此功能(使用 Mockito,但它也应该适用于 MockK)绑定 class 以查看我需要模拟哪些方法才能使其膨胀并 return 正确模拟视图。这些文件在 app/build/generated/data_binding_base_class_source_out/debug/out/your/package/databinding 中用于标准构建

这里是生成的数据绑定示例 class,在 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)

获取包含模拟视图的实际绑定实例。