包含自定义视图的Android UI测试片段

包含自定义视图的Android UI测试片段,android,testing,instrumentation,Android,Testing,Instrumentation,我需要测试一个包含自定义视图的片段。我刚开始编写UI测试,但遇到了一个错误: android.view.InflateException: Binary XML file line #36 in *****.staging.debug:layout/fragment_form: Binary XML file line #36 in *****.staging.debug:layout/fragment_form: Error inflating class *****.util.view.Br

我需要测试一个包含自定义视图的片段。我刚开始编写UI测试,但遇到了一个错误:

android.view.InflateException: Binary XML file line #36 in *****.staging.debug:layout/fragment_form: Binary XML file line #36 in *****.staging.debug:layout/fragment_form: Error inflating class *****.util.view.BrandedEditText
Caused by: android.view.InflateException: Binary XML file line #36 in *****.staging.debug:layout/fragmentn_form: Error inflating class *****.util.view.BrandedEditText
Caused by: java.lang.NoSuchMethodException: *****.util.view.BrandedEditText.<init> [class android.content.Context, interface android.util.AttributeSet]
FormFragment:

class FormFragment : BaseFragment(), Injectable {

    private val loadingDialog by lazy { LoadingDialog(requireContext()) }

    @Inject
    lateinit var viewModelFactory: DaggerViewModelFactory<FormViewModel>

    private val viewModel: FormViewModel by lazy {
        ViewModelProvider(
                this,
                viewModelFactory
        ).get(FormViewModel::class.java)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        val binding: FragmentFormBinding = setAndBindContentView(inflater, container, R.layout.fragment_form)
        binding.viewModel = viewModel
        return binding.root
    }

...

类FormFragment:BaseFragment(),可注入{
通过惰性{loadingDialog(requireContext())}加载私有val loadingDialog
@注入
lateinit变量viewModelFactory:DaggerViewModelFactory
private val viewModel:FormViewModel by lazy{
ViewModelProvider(
这
viewModelFactory
).get(FormViewModel::class.java)
}
覆盖创建视图(充气机:布局充气机,容器:ViewGroup?,savedInstanceState:Bundle?):视图{
val绑定:FragmentFormBinding=setAndBindContentView(充气机、容器、R.layout.fragment_表单)
binding.viewModel=viewModel
返回binding.root
}
...
基本片段:

abstract class BaseFragment : Fragment(), NavControllerOwner {

    protected var binding: ViewDataBinding? = null
    protected var isAlreadyBound = false

    protected val mainActivity: MainActivity?
        get() = activity as? MainActivity

    override val navController: NavController?
        get() = (activity as NavControllerOwner).navController

    @Suppress("UNCHECKED_CAST")
    protected fun <ViewBindingType : ViewDataBinding?> setAndBindContentView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            @LayoutRes layoutResId: Int
    ): ViewBindingType {
        binding?.let {
            isAlreadyBound = true
            binding?.lifecycleOwner = viewLifecycleOwner
            return it as ViewBindingType
        }
        binding = DataBindingUtil.inflate(inflater, layoutResId, container, false)
        binding?.lifecycleOwner = viewLifecycleOwner
        return binding as ViewBindingType
    }

    protected fun setContentView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            @LayoutRes layoutResID: Int
    ): View = inflater.inflate(layoutResID, container, false)
}
抽象类BaseFragment:Fragment(),NavControllerOwner{
受保护的变量绑定:ViewDataBinding?=null
受保护变量isAlreadyBound=false
受保护的val mainActivity:mainActivity?
get()=活动作为?main活动
覆盖val导航控制器:导航控制器?
get()=(作为NavControllerOwner的活动)。navController
@抑制(“未选中的_CAST”)
受保护的乐趣集和BindContentView(
充气机,
容器:视图组?,
@LayoutRes layoutResId:Int
):ViewBindingType{
捆绑?让我来{
isAlreadyBound=true
绑定?.lifecycleOwner=viewLifecycleOwner
将其作为ViewBindingType返回
}
绑定=数据绑定直到充气(充气器、布局器、容器、假)
绑定?.lifecycleOwner=viewLifecycleOwner
以ViewBindingType的形式返回绑定
}
受保护的趣味setContentView(
充气机,
容器:视图组?,
@LayoutRes layoutResID:Int
):视图=充气机。充气(layoutResID、容器、假)
}
自定义视图:

import android.content.Context
import android.os.Parcelable
import android.text.InputFilter
import android.text.InputFilter.LengthFilter
import android.text.InputType
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatEditText
import androidx.databinding.BindingAdapter
import com.google.android.material.textfield.TextInputLayout
import *****.R
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.view_branded_edit_text.view.*

/**
 * Styled edit text.  Use android:inputType for input type, and app:editTextTitle for the title.
 */
class BrandedEditText(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) {

    val editText: AppCompatEditText
        get() = brandedAppCompatEditText

    val inputLayout: TextInputLayout
        get() = textInputLayout

    init {
        View.inflate(context, R.layout.view_branded_edit_text, this)
        context.theme.obtainStyledAttributes(
                attrs,
                R.styleable.BrandedEditText,
                0, 0
        ).apply {
            title.text = getString(R.styleable.BrandedEditText_editTextTitle)
            brandedAppCompatEditText.inputType = getInt(R.styleable.BrandedEditText_android_inputType, InputType.TYPE_NULL)
            brandedAppCompatEditText.filters = arrayOf<InputFilter>(
                    LengthFilter(getInt(R.styleable.BrandedEditText_maxLength, Int.MAX_VALUE))
            )
            if (getBoolean(R.styleable.BrandedEditText_passwordToggleEnabled, false)) {
                inputLayout.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
            }
        }
    }

    fun setMaxLength(maxLength: Int) {
        brandedAppCompatEditText.filters = arrayOf<InputFilter>(
                LengthFilter(maxLength)
        )
    }

    override fun onSaveInstanceState(): Parcelable? = SavedState(super.onSaveInstanceState(), editText.text.toString())

    override fun onRestoreInstanceState(state: Parcelable?) {
        super.onRestoreInstanceState(state)
        editText.setText((state as? SavedState)?.text)
    }

    companion object {
        @JvmStatic
        @BindingAdapter("app:errorText")
        fun setErrorText(view: BrandedEditText, @StringRes textId: Int) {
            if (textId == 0x0) return
            view.inputLayout.error = view.context.getString(textId)
        }
    }

    @Parcelize
    data class SavedState(
            val superStateValue: Parcelable?,
            val text: String?
    ) : BaseSavedState(superStateValue)
}
导入android.content.Context
导入android.os.Parcelable
导入android.text.InputFilter
导入android.text.InputFilter.LengthFilter
导入android.text.InputType
导入android.util.AttributeSet
导入android.view.view
导入android.widget.FrameLayout
导入androidx.annotation.StringRes
导入androidx.appcompat.widget.AppCompatEditText
导入androidx.databinding.BindingAdapter
导入com.google.android.material.textfield.TextInputLayout
进口******.R
导入kotlinx.android.Parcelize.Parcelize
导入kotlinx.android.synthetic.main.view\u branded\u edit\u text.view*
/**
*样式化编辑文本。输入类型使用android:inputType,标题使用app:editTextTitle。
*/
类BrandedEditText(上下文:上下文,属性集):FrameLayout(上下文,属性集){
val editText:AppCompativeEditText
get()=BrandedAppCompativeText
val inputLayout:TextInputLayout
get()=textInputLayout
初始化{
查看。充气(上下文,右布局。查看\u品牌\u编辑\u文本,此)
context.theme.obtainStyledAttributes(
属性,
R.styleable.BrandedEditText,
0, 0
).申请{
title.text=getString(R.styleable.BrandedEditText\u editTextTitle)
BrandedAppCompativeText.inputType=getInt(R.styleable.BrandedEditText\u android\u inputType,inputType.TYPE\u NULL)
BrandedAppCompativeText.filters=arrayOf(
LengthFilter(getInt(R.styleable.BrandedEditText\u maxLength,Int.MAX\u VALUE))
)
if(getBoolean(R.styleable.BrandedEditText\u passwordToggleEnabled,false)){
inputLayout.endIconMode=TextInputLayout.END\u图标\u密码\u切换
}
}
}
fun setMaxLength(maxLength:Int){
BrandedAppCompativeText.filters=arrayOf(
长度过滤器(最大长度)
)
}
重写onSaveInstanceState():Parcelable?=SavedState(super.onSaveInstanceState(),editText.text.toString())
覆盖恢复安装状态(状态:可包裹?){
super.onRestoreInstanceState(状态)
editText.setText((状态为?SavedState)?.text)
}
伴星{
@JvmStatic
@BindingAdapter(“应用程序:errorText”)
fun setErrorText(视图:BrandedEditText,@StringRes textId:Int){
if(textId==0x0)返回
view.inputLayout.error=view.context.getString(textId)
}
}
@包裹
数据类存储状态(
val超级状态值:可包裹?,
val文本:字符串?
):BaseSavedState(超级状态值)
}

我尝试在src/androidTest目录中创建一个具有相同签名的类,这很有帮助。但是,为什么会发生此错误仍然是一个谜。此外,我认为每次发生类似错误时复制相同的代码并不完全正确
import android.content.Context
import android.os.Parcelable
import android.text.InputFilter
import android.text.InputFilter.LengthFilter
import android.text.InputType
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatEditText
import androidx.databinding.BindingAdapter
import com.google.android.material.textfield.TextInputLayout
import *****.R
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.view_branded_edit_text.view.*

/**
 * Styled edit text.  Use android:inputType for input type, and app:editTextTitle for the title.
 */
class BrandedEditText(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) {

    val editText: AppCompatEditText
        get() = brandedAppCompatEditText

    val inputLayout: TextInputLayout
        get() = textInputLayout

    init {
        View.inflate(context, R.layout.view_branded_edit_text, this)
        context.theme.obtainStyledAttributes(
                attrs,
                R.styleable.BrandedEditText,
                0, 0
        ).apply {
            title.text = getString(R.styleable.BrandedEditText_editTextTitle)
            brandedAppCompatEditText.inputType = getInt(R.styleable.BrandedEditText_android_inputType, InputType.TYPE_NULL)
            brandedAppCompatEditText.filters = arrayOf<InputFilter>(
                    LengthFilter(getInt(R.styleable.BrandedEditText_maxLength, Int.MAX_VALUE))
            )
            if (getBoolean(R.styleable.BrandedEditText_passwordToggleEnabled, false)) {
                inputLayout.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
            }
        }
    }

    fun setMaxLength(maxLength: Int) {
        brandedAppCompatEditText.filters = arrayOf<InputFilter>(
                LengthFilter(maxLength)
        )
    }

    override fun onSaveInstanceState(): Parcelable? = SavedState(super.onSaveInstanceState(), editText.text.toString())

    override fun onRestoreInstanceState(state: Parcelable?) {
        super.onRestoreInstanceState(state)
        editText.setText((state as? SavedState)?.text)
    }

    companion object {
        @JvmStatic
        @BindingAdapter("app:errorText")
        fun setErrorText(view: BrandedEditText, @StringRes textId: Int) {
            if (textId == 0x0) return
            view.inputLayout.error = view.context.getString(textId)
        }
    }

    @Parcelize
    data class SavedState(
            val superStateValue: Parcelable?,
            val text: String?
    ) : BaseSavedState(superStateValue)
}