在测试 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 看起来像这样。我正在使用 MockK 和 Robolectric 来获取应用程序上下文
@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)
获取包含模拟视图的实际绑定实例。
我正在尝试为使用 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 看起来像这样。我正在使用 MockK 和 Robolectric 来获取应用程序上下文
@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)
获取包含模拟视图的实际绑定实例。