前言
快速总结 ↬ Espresso 和 Mockito 等框架提供易于使用的API,可以更轻松地为各种场景编写测试。让我们介绍测试的基础知识和开发人员可以用来编写单元测试的框架。
在应用程序开发中,随着代码的迭代,会出现各种用例和交互。应用程序可能需要从服务器获取数据、与设备的传感器交互、访问本地存储或呈现复杂的用户界面。
编写测试时要考虑的重要一点是在设计新功能时出现的责任单元。单元测试应涵盖与单元的所有可能交互,包括标准交互和异常场景。
在本文中,我们将介绍测试和框架的基础知识,例如 Mockito 和 Espresso,开发人员可以使用它们来编写单元测试。我还将简要讨论如何编写可测试的代码。我还将解释如何开始在 Android 中进行本地和仪器测试。
测试基础
典型的单元测试包含三个阶段。
- 首先,单元测试初始化它想要测试的应用程序的一小部分。
- 然后,它对被测系统施加一些刺激,通常是通过调用它的方法。
- 最后,它观察结果行为。
如果观察到的行为与预期一致,则单元测试通过;否则失败,说明被测系统某处有问题。这三个单元测试阶段也称为rrange、ct和ssert ,或者简称为AAA 。理想情况下,该应用程序应包括三类测试:小型、中型和大型。
- 小型测试包括模拟每个主要组件并独立快速运行的单元测试。
- 中型测试是集成多个组件并在模拟器或真实设备上运行的集成测试。
- 大型测试是通过完成UI 工作流来运行的集成和 UI 测试,并确保关键的最终用户任务按预期工作。
注意: 仪器测试是一种集成测试。这些是在 Android设备或模拟器上运行的测试。这些测试可以访问仪器信息,例如被测应用的上下文。使用这种方法来运行具有模拟对象无法轻松满足的 Android依赖项的
编写小型测试可以让您快速解决故障,但很难确信通过测试将使您的应用程序正常工作。在应用程序中进行所有类别的测试很重要,尽管每个类别的比例可能因应用程序而异。一个好的单元测试应该易于编写、可读、可靠和快速。
下面是对 Mockito 和 Espresso 的简要介绍,它们使测试 Android 应用程序变得更加容易。
模拟
有各种模拟框架,但其中最流行的是Mockito:
Mockito 是一个模拟框架,味道非常好。它使您可以使用干净简单的 API 编写漂亮的测试。Mockito不会给您带来宿醉,因为测试非常易读,并且会产生干净的验证错误。
其流畅的 API 将测试前准备与测试后验证分开。如果测试失败,Mockito 会清楚地看到我们的期望与现实有什么不同!该库拥有编写完整测试所需的一切。
Espresso
Espresso 帮助您编写简洁、美观且可靠的 Android UI 测试。
下面的代码片段显示了 Espresso 测试的示例。当我们详细讨论仪器测试时,我们将在本教程后面使用相同的示例。
@Test
public void setUserName() {
onView(withId(R.id.name_field)).perform(typeText("Vivek Maskara"));
onView(withId(R.id.set_user_name)).perform(click());
onView(withText("Hello Vivek Maskara!")).check(matches(isDisplayed()));
}
Espresso 可以清晰地测试状态期望、交互和断言,而不会受到样板内容、自定义基础设施或凌乱的实施细节的干扰。每当您的测试调用onView()时,Espresso 都会等待执行相应的 UI 操作或断言,直到满足同步条件,这意味着:
- 消息队列为空,
- 当前没有实例AsyncTask正在执行任务,
- 闲置资源闲置。
这些检查确保测试结果可靠。
编写可测试的代码
对 Android 应用程序进行单元测试很困难,有时甚至是不可能的。好的设计,也只有好的设计,才能让单元测试更容易。以下是一些对于编写可测试代码很重要的概念。
避免将对象图构造与应用程序逻辑混合使用
在测试中,您希望实例化被测类并对类应用一些刺激并断言观察到了预期的行为。确保被测类不实例化其他对象,并且这些对象不实例化更多对象等等。为了有一个可测试的代码库,你的应用程序应该有两种类:
- 工厂,充满了“新”操作符,负责构建应用程序的对象图;
- 应用程序逻辑类,它们没有“新”运算符并负责完成工作。
构造函数不应该做任何工作
您将在测试中执行的最常见操作是对象图的实例化。所以,让自己轻松一点,让构造函数除了将所有依赖项分配到字段中之外什么都不做。在构造函数中做工作不仅会影响类的直接测试,还会影响试图间接实例化你的类的相关测试。
尽可能避免使用静态方法
测试的关键是存在可以转移正常执行流程的地方。需要接缝,以便您可以隔离测试单元。如果您构建一个只使用静态方法的应用程序,您将拥有一个过程应用程序。从测试的角度来看,静态方法的伤害程度取决于它在应用程序调用图中的位置。像这样的叶子方法Math.abs()不是问题,因为执行调用图到此结束。但是如果你在应用程序逻辑的核心中选择一个方法,那么方法后面的所有东西都将变得难以测试,因为没有办法插入测试替身
避免混淆关注
一个类应该只负责处理一个实体。在一个类中,一个方法应该只负责做一件事。例如,BusinessService应该只负责与 aBusiness而不是交谈BusinessReceipts。此外, 中的方法BusinessService可以是getBusinessProfile,但诸如 的方法createAndGetBusinessProfile对于测试来说并不理想。良好的设计必须遵循S OLID 设计原则:
- S:单一职责原则;
- O:开闭原则;
- L:里氏替换原则;
- I:接口隔离原则;
- D:依赖倒置原则。
在接下来的几节中,我们将使用我为本教程构建的一个非常简单的应用程序中的示例。该应用程序有一个EditText将用户名作为输入并在TextView单击按钮时在 a 中显示名称的应用程序。这是该应用程序的屏幕截图:
编写本地单元测试
单元测试可以在您的开发机器上本地运行,无需设备或模拟器。这种测试方法很有效,因为它避免了每次运行测试时都必须将目标应用程序和单元测试代码加载到物理设备或模拟器上的开销。除了 Mockito,您还需要为您的项目配置测试依赖项,以使用 JUnit 4 框架提供的标准 API。
设置开发环境
首先在您的项目中添加对 JUnit4 的依赖项。依赖是 type testImplementation,这意味着依赖只需要编译项目的测试源。
testImplementation 'junit:junit:4.12'
我们还需要 Mockito 库来简化与 Android 依赖项的交互。
testImplementation "org.mockito:mockito-core:$MOCKITO_VERSION"
确保在添加依赖项后同步项目。默认情况下,Android Studio 应该已经为单元测试创建了文件夹结构。如果没有,请确保存在以下目录结构:
<Project Dir>/app/src/test/java/com/maskaravivek/testingExamples
创建你的第一个单元测试
假设您要测试displayUserName. UserService为简单起见,该函数只是对输入进行格式化并将其返回。在现实世界的应用程序中,它可以进行网络调用以获取用户配置文件并返回用户名。
@Singleton
class UserService @Inject
constructor(private var context: Context) {
fun displayUserName(name: String): String {
val userNameFormat = context.getString(R.string.display_user_name)
return String.format(Locale.ENGLISH, userNameFormat, name)
}
}
UserServiceTest我们将首先在我们的测试目录中创建一个类。UserService该类使用Context,为了测试目的,需要对其进行模拟。Mockito 提供了一种@Mock用于模拟对象的表示法,可以按如下方式使用:
@Mock internal var context: Context? = null
同样,您需要模拟构造UserService类实例所需的所有依赖项。在测试之前,您需要初始化这些模拟并将它们注入到UserService类中。
- @InjectMock创建该类的一个实例并将标有注释的模拟@Mock注入其中。
- MockitoAnnotations.initMocks(this);初始化那些用 Mockito 注释注释的字段。
这是如何完成的:
class UserServiceTest {
@Mock internal var context: Context? = null
@InjectMocks internal var userService: UserService? = null
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
}
}
现在您已经完成了测试类的设置。让我们在这个类中添加一个测试来验证函数的displayUserName功能。这是测试的样子:
@Test
fun displayUserName() {
doReturn("Hello %s!").`when`(context)!!.getString(any(Int::class.java))
val displayUserName = userService!!.displayUserName("Test")
assertEquals(displayUserName, "Hello Test!")
}
测试使用语句在调用doReturn().when()a 时提供响应。context.getString()对于任何输入整数,它将返回相同的结果,“Hello %s!”。我们可以更具体地让它只为特定的字符串资源 ID 返回这个响应,但为了简单起见,我们对任何输入都返回相同的响应。最后,这是测试类的样子:
class UserServiceTest {
@Mock internal var context: Context? = null
@InjectMocks internal var userService: UserService? = null
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
}
@Test
fun displayUserName() {
doReturn("Hello %s!").`when`(context)!!.getString(any(Int::class.java))
val displayUserName = userService!!.displayUserName("Test")
assertEquals(displayUserName, "Hello Test!")
}
}
运行单元测试
为了运行单元测试,您需要确保 Gradle 是同步的。要运行测试,请单击 IDE 中的绿色播放图标。
当单元测试运行成功或失败时,您应该在屏幕底部的“运行”菜单中看到:
你已经完成了你的第一个单元测试!
编写仪器测试
Instrumentation 测试最适合在 Activity 运行时检查 UI 组件的值。例如,在上面的示例中,我们要确保在单击TextView后显示正确的用户名。Button它们在物理设备和模拟器上运行,并且可以利用 Android 框架 API 和支持 API,例如 Android 测试支持库。我们将使用 Espresso 在主线程上执行操作,例如按钮单击和文本更改。
设置开发环境
添加对 Espresso 的依赖项:
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
androidTest仪器测试在文件夹中创建。
<Project Dir>/app/src/androidTest/java/com/maskaravivek/testingExamples
如果您想测试一个简单的活动,请在与活动相同的包中创建您的测试类。
创建您的第一个仪器测试
让我们从创建一个简单的活动开始,该活动将名称作为输入,并在单击按钮时显示用户名。上述活动的代码非常简单:
class MainActivity : AppCompatActivity() {
var button: Button? = null
var userNameField: EditText? = null
var displayUserName: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
setContentView(R.layout.activity_main)
initViews()
}
private fun initViews() {
button = this.findViewById(R.id.set_user_name)
userNameField = this.findViewById(R.id.name_field)
displayUserName = this.findViewById(R.id.display_user_name)
this.button!!.setOnClickListener({
displayUserName!!.text = "Hello ${userNameField!!.text}!"
})
}
}
要为 . 创建一个测试MainActivity,我们将首先在目录MainActivityTest下创建一个类。androidTest在类中添加AndroidJUnit4注解,表示该类中的测试将使用默认的 Android 测试运行器类。
@RunWith(AndroidJUnit4::class)
class MainActivityTest {}
接下来,ActivityTestRule在类中添加一个。此规则提供单个活动的功能测试。在测试期间,您将能够使用从getActivity().
@Rule @JvmField var activityActivityTestRule = ActivityTestRule(MainActivity::class.java)
现在您已经完成了测试类的设置,让我们添加一个测试,通过单击“设置用户名”按钮来验证用户名是否显示。
@Test
fun setUserName() {
onView(withId(R.id.name_field)).perform(typeText("Vivek Maskara"))
onView(withId(R.id.set_user_name)).perform(click())
onView(withText("Hello Vivek Maskara!")).check(matches(isDisplayed()))
}
上面的测试很容易理解。它首先模拟EditText在TextView.
最终的测试类如下所示:
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@Rule @JvmField var activityActivityTestRule = ActivityTestRule(MainActivity::class.java)
@Test
fun setUserName() {
onView(withId(R.id.name_field)).perform(typeText("Vivek Maskara"))
onView(withId(R.id.set_user_name)).perform(click())
onView(withText("Hello Vivek Maskara!")).check(matches(isDisplayed()))
}
}
运行仪器测试
就像单元测试一样,点击 IDE 中的绿色播放按钮来运行测试。
单击播放按钮后,应用程序的测试版本将安装在模拟器或设备上,测试将在其上自动运行。
使用 Dagger、Mockito 和 Espresso 进行仪器测试
Espresso 是最受欢迎的 UI 测试框架之一,具有良好的文档和社区支持。Mockito 确保对象执行预期的操作。Mockito 还可以很好地与 Dagger 等依赖注入库配合使用。模拟依赖项允许我们单独测试一个场景。到目前为止,我们MainActivity还没有使用任何依赖注入,因此,我们能够非常轻松地编写我们的 UI 测试。为了让事情变得更有趣,让我们注入UserService并MainActivity使用它来获取要显示的文本。
class MainActivity : AppCompatActivity() {
var button: Button? = null
var userNameField: EditText? = null
var displayUserName: TextView? = null
@Inject lateinit var userService: UserService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
setContentView(R.layout.activity_main)
initViews()
}
private fun initViews() {
button = this.findViewById(R.id.set_user_name)
userNameField = this.findViewById(R.id.name_field)
displayUserName = this.findViewById(R.id.display_user_name)
this.button!!.setOnClickListener({
displayUserName!!.text = userService.displayUserName(userNameField!!.text.toString())
})
}
}
在图中使用 Dagger,我们必须在编写仪器测试之前设置一些东西。想象一下,该displayUserName函数在内部使用一些 API 来获取用户的详细信息。不应出现由于服务器故障而导致测试未通过的情况。为了避免这种情况,我们可以使用依赖注入框架 Dagger 和用于网络的 Retrofit。
在应用程序中设置 DAGGER
我们将快速设置 Dagger 所需的基本模块和组件。如果您不熟悉 Dagger,请查看Google 的文档。build.gradle我们将开始在文件中添加使用 Dagger 的依赖项。
implementation "com.google.dagger:dagger-android:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
在类中创建一个组件Application,并添加将在我们的项目中使用的必要模块。我们需要在MainActivity我们的应用程序中注入依赖项。我们将@Module在活动中添加一个用于注入。
@Module
abstract class ActivityBuilder {
@ContributesAndroidInjector
internal abstract fun bindMainActivity(): MainActivity
}
该类AppModule将提供应用程序所需的各种依赖项。对于我们的示例,它将仅提供Contextand的一个实例UserService。
@Module
open class AppModule(val application: Application) {
@Provides
@Singleton
internal open fun provideContext(): Context {
return application
}
@Provides
@Singleton
internal open fun provideUserService(context: Context): UserService {
return UserService(context)
}
}
该类AppComponent允许您为应用程序构建对象图。
@Singleton
@Component(modules = [(AndroidSupportInjectionModule::class), (AppModule::class), (ActivityBuilder::class)])
interface AppComponent {
@Component.Builder
interface Builder {
fun appModule(appModule: AppModule): Builder
fun build(): AppComponent
}
fun inject(application: ExamplesApplication)
}
创建一个返回已构建组件的方法,然后将此组件注入onCreate().
open class ExamplesApplication : Application(), HasActivityInjector {
@Inject lateinit var dispatchingActivityInjector: DispatchingAndroidInjector<Activity>
override fun onCreate() {
super.onCreate()
initAppComponent().inject(this)
}
open fun initAppComponent(): AppComponent {
return DaggerAppComponent
.builder()
.appModule(AppModule(this))
.build()
}
override fun activityInjector(): DispatchingAndroidInjector<Activity>? {
return dispatchingActivityInjector
}
}
在测试应用程序中设置 DAGGER #
为了模拟来自服务器的响应,我们需要创建一个Application扩展上述类的新类。
class TestExamplesApplication : ExamplesApplication() {
override fun initAppComponent(): AppComponent {
return DaggerAppComponent.builder()
.appModule(MockApplicationModule(this))
.build()
}
@Module
private inner class MockApplicationModule internal constructor(application: Application) : AppModule(application) {
override fun provideUserService(context: Context): UserService {
val mock = Mockito.mock(UserService::class.java)
`when`(mock!!.displayUserName("Test")).thenReturn("Hello Test!")
return mock
}
}
}
正如您在上面的示例中所看到的,我们使用 Mockito 来模拟UserService和假设结果。我们仍然需要一个新的运行程序,它将指向具有覆盖数据的新应用程序类。
class MockTestRunner : AndroidJUnitRunner() {
override fun onCreate(arguments: Bundle) {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().permitAll().build())
super.onCreate(arguments)
}
@Throws(InstantiationException::class, IllegalAccessException::class, ClassNotFoundException::class)
override fun newApplication(cl: ClassLoader, className: String, context: Context): Application {
return super.newApplication(cl, TestExamplesApplication::class.java.name, context)
}
}
接下来,您需要更新build.gradle文件以使用MockTestRunner.
android {
...
defaultConfig {
...
testInstrumentationRunner ".MockTestRunner"
}
}
运行测试
TestExamplesApplication新的所有测试MockTestRunner都应该在androidTest包中添加。这种实现使测试完全独立于服务器,并使我们能够操纵响应。有了上面的设置,我们的测试类根本不会改变。运行测试时,应用程序将使用TestExamplesApplication代替ExamplesApplication,因此将使用 的模拟实例UserService。
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@Rule @JvmField var activityActivityTestRule = ActivityTestRule(MainActivity::class.java)
@Test
fun setUserName() {
onView(withId(R.id.name_field)).perform(typeText("Test"))
onView(withId(R.id.set_user_name)).perform(click())
onView(withText("Hello Test!")).check(matches(isDisplayed()))
}
}
当您单击 IDE 中的绿色播放按钮时,测试将成功运行。
您已成功设置 Dagger 并使用 Espresso 和 Mockito 运行测试。
结论
我们已经强调了提高代码覆盖率的最重要方面是编写可测试的代码。Espresso 和 Mockito 等框架提供了易于使用的 API,可以更轻松地为各种场景编写测试。测试应该单独运行,模拟依赖关系使我们有机会确保对象执行预期的操作。
有多种 Android 测试工具可供使用,随着生态系统的成熟,设置可测试环境和编写测试的过程将变得更加容易。
编写单元测试需要一定的纪律、专注和额外的努力。通过针对您的代码创建和运行单元测试,您可以轻松验证各个单元的逻辑是否正确。在每次构建后运行单元测试可帮助您快速捕获和修复由应用程序代码更改引入的软件回归。