eclipse中自定义视图
Originally published here.
最初在这里发表。
前言 (Preface)
In this short article, I want to propose a method on unit testing an Android View.
在这篇简短的文章中,我想提出一种对Android View进行单元测试的方法。
Anybody who ever wrote UI tests knows it’s quite slow (both in terms of implementation and in terms of execution) and prone to errors and number of problems (especially running on CI). Robolectric improves the situation quite a bit but still has its own problems.
曾经编写过UI测试的任何人都知道它相当慢(无论是在实现方面还是在执行方面),而且容易出错和出现问题的数量(尤其是在CI上运行)。 Robolectric改善了情况,但仍然存在其自身的问题。
I want to propose an alternative solution, which, while not being applicable everywhere, might provide a faster, better and more stable option for UI testing.
我想提出一种替代解决方案,尽管它并非在所有地方都适用,但可以为UI测试提供更快,更好和更稳定的选择。
给我代码 (Just give me the code)
If you would rather just play with code here’s the repo that contains all the code used in this article.
如果你宁愿只是代码玩,这里的回购包含在本文中使用的所有代码。
高级概述 (High level overview)
So what is the approach exactly?
那么到底是什么方法呢?
The main idea is that we don’t need to check if our UI is displayed correctly (most of the time) what we really need is to make sure that we’ve set proper variables or called proper functions with proper arguments.
主要思想是,我们不需要(大部分时间)检查UI是否正确显示,我们真正需要的是确保已设置适当的变量或使用适当的参数调用了适当的函数。
What does it mean? It means that if we can make sure that our code called:
这是什么意思? 这意味着如果我们可以确保我们的代码调用:
view.setBackgroundColor(R.color.red)
view.setBackgroundColor(R.color.red)
we don’t need to check that our view became red since we assume that the framework works correctly. And well, if it doesn’t, it is nothing you can fix directly anyway, you can only work around it.
因为我们假设框架可以正常工作,所以我们不需要检查视图是否变为红色。 而且,如果不能,那么无论如何您都无法直接修复它,只能解决它。
The solution I want to present utilises custom Android view and Mockk, which allows us to achieve exactly what we want — ability to check that methods were called, without dragging the whole Android framework to tests with us. Note that the technique is applicable to Activity, but, since you can’t create an instance of Activity manually, you still will need either Robolectric or instrumentation to provide you with that, which removes much of the benefits of the approach.
我要介绍的解决方案利用自定义的Android视图和Mockk,这使我们能够准确实现所需的功能-能够检查方法是否被调用,而无需拖动整个Android框架来进行测试。 请注意,该技术适用于Activity,但是由于您无法手动创建Activity的实例,因此仍然需要Robolectric或工具来为您提供该实例,这消除了该方法的许多优点。
自订检视 (Custom view)
I will start with the presentation of the code since there is not a lot of it:
因为代码很多,所以我将从代码的介绍开始:
And layout looks like:
布局看起来像:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:orientation="vertical"
tools:layout_width="match_parent"
tools:layout_height="match_parent"
tools:parentTag="com.syllogismobile.unittestingview.CustomView">
<TextView android:id="@+id/fact"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center_horizontal|top"
android:textSize="@dimen/default_text_size"
android:layout_margin="@dimen/standard_margin"
android:layout_weight="8"
android:ellipsize="end"
tools:text="A cat's jaw has only up and down motion; it does not have any lateral, side to side motion, like dogs and humans."/>
<TextView android:id="@+id/likes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:textSize="@dimen/default_text_size"
android:textColor="@android:color/holo_red_light"
android:layout_margin="@dimen/standard_margin"
android:layout_weight="1"
tools:text="1"/>
<Button android:id="@+id/likeButton"
android:layout_width="match_parent"
android:layout_height="@dimen/default_button_height"
android:layout_margin="@dimen/standard_margin"
android:layout_gravity="bottom"
android:text="@string/like" />
</merge>
Note that in init we have only a LayoutInflater call and it’s per design. We will use spyk from Mockk to replace Android functionality with mock, while still maintaining our logic intact. The problem is that for spyk you need an already created instance which means that we cannot do anything except for the basic UI initialization in init. And also this is the reason why we move most of the actual setup code to initialize. It’s one of the drawbacks of the method: you’ll have to split logic and call initialization manually somewhere down the road.
请注意,在初始化过程中,我们只有一个LayoutInflater调用,并且仅针对每个设计。 我们将使用Mockk的spyk来用模拟代替Android功能,同时仍保持逻辑不变。 问题是对于spyk,您需要一个已经创建的实例,这意味着除了init中的基本UI初始化之外,我们无法执行任何操作。 这也是我们移动大多数实际设置代码进行初始化的原因。 这是该方法的缺点之一:您将不得不拆分逻辑并在以后的某个位置手动调用初始化。
将视图集成到活动中 (Integrating view into Activity)
This is a no-brainer, I add it here for the sake of completeness.
这是理所当然的,为了完整起见,我在这里添加它。
Layout:
布局:
<?xml version="1.0" encoding="utf-8"?>
<com.syllogismobile.unittestingview.CustomView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/customView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" />
Code:
码:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
customView.initialize()
updateCustomView()
}
private fun updateCustomView() {
val model = CustomViewModel(
"A cat's jaw has only up and down motion; it does not have any lateral, side to side motion, like dogs and humans.",
10
)
customView.update(model)
}
}
As you can see, it’s just a wrapper to present our CustomView on the actual device with some mock data.
如您所见,它只是一个包装,用于在实际设备上提供一些模拟数据的CustomView。
测试中 (Testing)
Well, finally, we’ve arrived at the meat of the article. Let me present the testing code:
好了,最后,我们到了本文的重点。 让我介绍一下测试代码:
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.LinearLayout.VERTICAL
import android.widget.TextView
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.android.synthetic.main.custom_view.view.*
import org.junit.AfterClass
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
class CustomViewUnitTests {
@MockK private lateinit var context: Context
@MockK private lateinit var layoutInflater: LayoutInflater
@MockK private lateinit var fact: TextView
@MockK private lateinit var likes: TextView
@MockK private lateinit var likeButton: Button
private lateinit var subject: CustomView
@Before
fun setUp() {
MockKAnnotations.init(this, relaxed = true)
every { LayoutInflater.from(context) } returns layoutInflater
every { layoutInflater.inflate(any<Int>(), any()) } returns mockk()
subject = spyk(CustomView(context))
every { subject.context } returns context
every { subject.orientation = any() } returns Unit
every { subject.fact } returns fact
every { subject.likes } returns likes
every { subject.likeButton } returns likeButton
subject.initialize()
}
@Test
fun testInit() {
every { likes.text } returns "1"
val clickSlot = slot<View.OnClickListener>()
verify(exactly = 1) {
likeButton.setOnClickListener(capture(clickSlot))
subject.orientation = VERTICAL
}
clickSlot.captured.onClick(likeButton)
verify(exactly = 1) { likes.text = "2" }
}
@Test
fun testUpdatesCorrectly() {
val mockModel = CustomViewModel("text", 10)
subject.update(mockModel)
verify(exactly = 1) {
fact.text = "text"
likes.text = "10"
}
}
companion object {
@JvmStatic
@BeforeClass
fun setUpClass() {
mockkStatic(LayoutInflater::class)
}
@JvmStatic
@AfterClass
fun tearDownClass() {
unmockkStatic(LayoutInflater::class)
}
}
}
I think it’s better to start from the bottom, where the companion object is. As you can see, we define two functions: to run before the suite and to run after it. In first we make sure LayoutInflater is mocked (so we can mock LayoutInflater.from method later on) and in the second one, we unmock it to make sure other tests are not screwed. In the real scenario, you probably would have those methods in the base abstract class for all View tests.
我认为最好从底部开始,即伴随对象所在的位置。 如您所见,我们定义了两个函数:在套件之前运行,在套件之后运行。 首先,我们确保对LayoutInflater进行了模拟(以便以后可以对LayoutInflater.from方法进行模拟),而在第二项中,我们对其进行了模拟,以确保未进行其他测试。 在实际情况下,您可能会在所有View测试的基本抽象类中拥有这些方法。
Next, let’s look at the setUp method at the top of the suite. Here we initialize mocks declared above with @MockK annotation, create a subject for tests with spyk and then set up views for our subject with mocks. This is the second drawback of the method — since there is no “real” layout in tests we need to mock every single UI component we want to verify in tests. That can create quite a bit of boilerplate code, but at the same time allows you to fine-tune code to your needs.
接下来,让我们看看套件顶部的setUp方法。 在这里,我们使用@MockK注释初始化上面声明的模拟,使用spyk创建测试的主题,然后使用模拟为主题设置视图。 这是该方法的第二个缺点-由于测试中没有“真实”布局,因此我们需要模拟要在测试中验证的每个UI组件。 这可以创建很多样板代码,但同时允许您根据需要微调代码。
After this, tests themselves are quite trivial: in the first we verify logic in initialize method, in the second we verify the update method updates UI correctly.
在此之后,测试本身就变得微不足道了:首先,我们验证initialize方法中的逻辑,而在第二步中,我们验证update方法正确更新了UI。
结束语 (Closing words)
I understand that this approach certainly won’t work for everyone, and it has its drawbacks. However, I do hope it will provide at least some developers with a better way to handle UI testing and maybe will spark a discussion that will result in a much better solution all together!
我知道这种方法肯定不会对每个人都有效,并且它有缺点。 但是,我确实希望它能为至少一些开发人员提供更好的方式来处理UI测试,并可能引发讨论,从而共同带来更好的解决方案!
On this note, I would like to wish you all the best and, hopefully, I will see you in the next article!
在此,我谨祝您一切顺利,并希望在下一篇文章中见到您!
翻译自: https://levelup.gitconnected.com/unit-testing-custom-view-in-android-56cab3eb0d7e
eclipse中自定义视图
所有评论(0)