安卓敏捷教程(一)
一、简介
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-9701-8_1) contains supplementary material, which is available to authorized users.
一段时间以来,敏捷开发一直是 Android 开发者的问题。已经有很多测试用户界面(UI)的方法,比如 Robotium 或 Monkey Runner,但是在 Android Studio 1.1 之前,单元测试很难使用,很难配置,并且在 Android 平台上实现很有挑战性。
毫无疑问,Google 会说在过去你可以使用 JUnit3 风格的单元测试。但是对于任何从事传统 Java 开发的人来说,这是一个戏剧性的倒退。开发人员会不小心使用许多第三方工具拼凑出一个 JUnit4 开发环境。更有可能的是,他们会简单地放弃,因为越来越多的互不兼容的库依赖最终会把他们拖垮。因为对于 Android 开发者来说根本没有工具箱,移动平台上的敏捷开发是不成熟的,让人想起了 21 世纪初的 Java 开发。
谢天谢地,这一切都改变了——Android 现在支持 JUnit4,Android 开发人员现在可以回到单元测试上来了。Android JUnit4 测试世界还处于早期阶段,文档还很少,所以在本书中,我们将展示使用 Android Studio 启动和运行单元测试的实用方法。我们还将看看如何通过其他特定于 UI 的 Android 测试库(如 Espresso)来补充这一点,从而为 Android 开发人员创建一个完整的敏捷测试框架。
你好,世界单元测试
在我们继续之前,让我们看一个简单的单元测试。出于演示的目的,我们可以使用 Google Calculator 示例中的 Add 方法,该方法可从 https://github.com/googlesamples/android-testing 获得(参见清单 1-1 )。
Listing 1-1. Add Method from Google’s Calculator Example
public double add(double firstOperand, double secondOperand) {
return firstOperand + secondOperand;
}
清单 1-2 展示了一个非常简单的单元测试,它测试 Add 方法是否能正确地将两个数相加。
Listing 1-2. Test Method for Add Method from Calculator Example
@Test
public void calculator_CorrectAdd_ReturnsTrue() {
double resultAdd = mCalculator.add(3, 4);
assertEquals(7, resultAdd, 0);
}
单元测试使用断言来确保方法提供预期的结果。在这种情况下,我们使用assertEquals来查看当 3 加 4 时Add方法是否返回 7。如果测试成功,那么我们应该会看到一个积极的或绿色的结果,如果没有,那么我们会在 Android Studio 中看到一个红色的结果。
了解使用敏捷方法进行 Android 开发的好处
如果你是敏捷开发的新手,你可能想知道敏捷如何改进开发过程。
最基本的,敏捷的,特别是单元测试,可以帮助你
- 在开发过程的早期发现更多的错误
- 自信地做出更多改变
- 内置回归测试
- 延长代码库的寿命
如果你编写单元测试,并且它们覆盖了你代码的重要部分,那么你将会发现更多的错误。您可以进行简单的修改来整理代码,或者进行更大范围的架构修改,运行您的单元测试,并且,如果它们都通过了,确信您没有引入任何微妙的缺陷。你写的单元测试越多,无论何时你改变代码,你就越能回归测试你的应用。一旦你有了大量的单元测试,那么它就变成了一个回归测试套件,让你有信心去做你不会尝试的事情。
单元测试意味着你不必再抱着“不要管”的心态去编程。您现在可以进行重大更改(更改为新的数据库、更新您的后端应用编程接口(API)、更改为新的材料设计主题等)。)并确信您的应用的行为与您做出更改之前是一样的,因为所有的测试都会执行,没有任何错误。
探索 Android 的敏捷测试金字塔
在你的测试套件中,有几种类型的测试是你需要的,以确保你的应用得到充分的测试。您应该对组件或方法级别的功能进行单元测试,对任何后端 RESTful APIs 进行 API 或验收测试,对 Android 活动和一般应用工作流进行 GUI(图形用户界面)测试。
经典的敏捷测试金字塔最早出现在 Mike Cohn 所著的《成功运用敏捷》(Pearson Education,2010)一书中。这是你的应用需要的每种测试的相对数量的一个很好的指南(见图 1-1 )。
图 1-1。
Agile Test Pyramid
在 Android 中创建 Hello World 单元测试
在下面的例子中,我们展示了如何在 Android Studio 中创建简单的单元测试示例。假设在计算器 Android 应用中添加两个数字工作正常,这应该返回 true。
若要设置和运行单元测试,您需要执行以下任务:
- 先决条件:Gradle 版本 1.1.x 的 Android 插件
- 创建
src/test/java文件夹 - 在
build.gradle(app)文件中添加 JUnit:4:12 依赖 - 在构建变体中选择单元测试的测试工件
- 创建单元测试
- 右键单击测试运行测试
点击文件➤项目结构,并确保 Android 插件版本高于 1.1。在图 1-2 中,Android 插件版本是 1.2.3,所以我们准备好了。
Figure 1-2.
接下来我们需要为我们的单元测试代码创建src/test/java文件夹。目前,这似乎是硬编码到这个目录。所以切换到项目视图来查看文件结构并创建文件夹(见图 1-3 )。或者,在 Windows 中使用文件资源管理器创建文件夹,或者在 Mac 上使用终端窗口中的命令行进行更改。当你回到 Android Studio 中的 Android 视图时,如果文件夹没有显示出来,也不用担心。当我们在“构建变体”窗口中切换到单元测试时,它们就会显示出来。
图 1-3。
Change to Project view
将junit库添加到build.gradle (app)文件的依赖项部分,如图 1-4 所示。
图 1-4。
Modify the build.gradle file
在构建变体中选择单元测试测试工件,并使用调试构建(参见图 1-5 )。当你在应用的 Android 视图中时,测试代码目录现在也应该出现了。
图 1-5。
Choose Unit Tests in Build Variant
为我们的简单示例创建单元测试代码。我们需要导入org.junit.Before,这样我们就可以创建一个Calculator对象。我们需要导入org.junit.Test来告诉 Android Studio 我们正在进行单元测试。由于我们要做一个assertEquals,我们还需要导入org.junit.Assert.assertEquals(参见清单 1-3 )。
Listing 1-3. Unit Test Code
package com.riis.calculatoradd;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class CalculatorTest {
private``Calculator``mCalculator
@Before
public void setUp() {
mCalculator``=``new
}
@Test
public void calculator_CorrectAdd_ReturnsTrue() {
double``resultAdd =``mCalculator
assertEquals("adding 3 + 4 didn’t work this time", 7, resultAdd, 0);
}
}
右键单击CalculatorTest java 文件并选择 Run‘calculator test’来运行测试(参见图 1-6 )。
图 1-6。
Running the unit test
您可以在运行窗口中看到测试结果(参见图 1-7 )。您可能还想单击配置齿轮并选择 Show Statistics 来查看测试需要多长时间。
图 1-7。
Test results
如果测试成功,它们显示为绿色,任何产生错误的都显示为红色。在您继续任何编码之前,您的所有测试都应该是绿色的。
GUI 测试
单元测试的真正美妙之处在于,你不需要仿真器或物理设备来进行测试。但是,如果我们回头看看我们的敏捷测试金字塔(图 1-1 ,我们知道我们将需要一些 GUI 测试。请记住,GUI 测试是对活动的测试,而单元测试是对代码中个别方法的测试。我们不需要像单元测试那样多的 GUI 测试,但是我们仍然需要测试每一个活动,包括快乐的路径和不快乐的路径。
当谈到测试 GUI 时,我们有几个框架可供选择:我们可以使用 Android JUnit3 框架、Google 的 Espresso、UIAutomator、Robotium 或一些黄瓜类型的 Android 框架,如 Calabash。在本书中,我们将使用谷歌的 Espresso,因为它快速而容易设置,并且它也支持 Gradle 和 Android Studio。但是你的作者在过去使用过其他的框架,它们都有它们的好处。
Espresso 有三个组件:视图匹配器、视图操作和视图断言。ViewMatchers 用于查找视图,ViewActions 允许您对视图做一些事情,ViewAssertions 类似于单元测试断言——它们允许您断言视图中的值是否是您所期望的。
清单 1-4 展示了一个简单的 Espresso GUI 测试示例。我们再次添加两个数字,但是这一次我们是通过与 GUI 交互来完成的,而不是调用底层方法。
Listing 1-4. Adding Two Numbers Using Espresso
public void testCalculatorAdd() {
onView(withId(R.id.``operand_one_edit_text``)).perform(typeText(``THREE
onView(withId(R.id.``operand_two_edit_text``)).perform(typeText(``FOUR
onView(withId(R.id.``operation_add_btn
onView(withId(R.id.``operation_result_text_view``)).check(matches(withText(``RESULT
}
在这个例子中,withId(R.id.operand_one_edit_text)是代码中的一个视图匹配器,而perform(typeText(THREE)是一个 ViewAction。最后check(matches(withText(RESULT))是 ViewAssertion。
创建 Hello,World GUI 测试
这次我们将展示如何在 Android Studio 中创建简单的 GUI 测试示例。与单元测试一样,假设计算器 Android 应用中的两个数字相加正常,那么这个测试应该返回 true。
要设置和运行 GUI 测试,您需要执行以下任务:
- 先决条件:安装 Android 支持库
- 将测试类放在
src/androidTest/java文件夹中 - 在
build.gradle(app)文件中添加 Espresso 依赖 - 在构建变体中选择 Android 测试工具测试工件
- 创建 GUI 测试
- 右键单击测试运行测试
点击工具➤安卓➤ SDK 管理器,点击 SDK 工具选项卡,确保安装了安卓支持库(见图 1-8 )。
图 1-8。
Android SDK Manager
默认情况下,当您使用项目向导创建项目时,Android Studio 会创建一个src/androidTest/java文件夹,因此您不必创建任何新目录。如果你看不到它,那么检查构建变体窗口中的测试工件是否被设置为 Android Instrumentation Tests(参见图 1-9 )。
图 1-9。
Build Variant test artifacts
将下面的 Espresso 库(参见清单 1-5 )添加到依赖部分的build.gradle (app)文件中,然后单击立即同步链接。打开 Gradle 控制台,因为这可能需要一两分钟的时间。
Listing 1-5. Espresso Libraries
dependencies {
androidTestCompile ’com.android.support.test:testing-support-lib:0.1’
androidTestCompile ’com.android.support.test.espresso:espresso-core:2.0’
}
清单 1-6 中的代码展示了我们如何设置和运行 GUI 测试来添加 3 + 4,以及我们如何断言这是 7.0。为了测试 Android 活动,我们需要用ActivityInstrumentationTestCase2类扩展CalculatorAddTest。这允许你控制活动。我们在使用getActivity()调用的setUp()方法中实现了这一点。
Listing 1-6. Adding Two numbers Using Espresso
import android.test.ActivityInstrumentationTestCase2;
import static``android.support.test.espresso.Espresso.``onView
import static android.support.test.espresso.action.ViewActions.click;
import static``android.support.test.espresso.action.ViewActions.``typeText
import static``android.support.test.espresso.assertion.ViewAssertions.``matches
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
public class``CalculatorAddTest``extends
public static final String THREE = "3"
public static final String FOUR = "4"
public static final String RESULT = "7.0"
public CalculatorAddTest() {
super``(CalculatorActivity.``class
}
@Override
protected void``setUp()``throws
super .setUp();
getActivity();
}
public void testCalculatorAdd() {
onView``(``withId``(R.id.``operand_one_edit_text``)).perform(``typeText``(``THREE``));
onView``(``withId``(R.id.``operand_two_edit_text``)).perform(``typeText``(``FOUR``));
onView ( withId (R.id. operation_add_btn )).perform( click ());
onView``(``withId``(R.id.``operation_result_text_view``)).check(``matches``(``withText``(``RESULT
}
}
在代码中,我们首先连接到 Calculator 活动,然后使用 ViewMatcher 和 ViewActions 将数字 3 和 4 放在正确的文本字段中。代码然后使用 ViewAction 单击 Add 按钮,最后我们使用 ViewAssertion 确保答案是预期的 7.0。请注意,GUI 将结果显示为双精度值,因此它是 7.0,而不是您可能期望的 7(参见图 1-10 )。
图 1-10。
Calculator app
图 1-11 显示了结果。在这种情况下,它们看起来非常类似于单元测试中的那些,但是模拟器需要更长的时间来启动。
图 1-11。
Espresso results
摘要
在这一章中,我们看了 Android 平台上单元测试和 GUI 测试的当前状态。在本书的其余部分,我们将更详细地探讨敏捷测试,这样你就可以看到如何将这些技术应用到你的应用中,以产生更干净、更快、缺陷更少的代码。
二、Android 单元测试
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-9701-8_2) contains supplementary material, which is available to authorized users.
在 Android Studio 整合 JUnit4 之前,Google 的实现是标准和特定于 Android 的单元测试的奇怪混合。JUnit4 的当前版本是 JUnit 标准的一个更加普通的实现(更多信息请参见 http://junit.org ,源代码请参见 https://github.com/junit-team/junit )。我们在 build.gradle 文件中加载的 JUnit 的当前推荐版本是 4.12
Android 断言
在我们的 Hello,World 示例中,我们使用了assertEquals断言,但是在 JUnit 4.12 中我们也可以使用其他断言(参见表 2-1 )。
表 2-1。
Assertions
| 主张 | 描述 | | --- | --- | | `assertEquals` | 测试两个值是否相同 | | `assertTrue` | 测试布尔条件为真 | | `assertFalse` | 测试布尔条件为假 | | `assertNull` | 检查对象是否为空 | | `assertNotNull` | 检查对象是否不为空 | | `assertSame` | 测试两个值是否引用同一个对象引用 | | `assertNotSame` | 测试两个值是否不引用同一个对象引用 | | `assertThat` | 测试第一个值(对象)是否匹配第二个值(或匹配器) | | `fail` | 测试应该总是失败 |如果您添加了 Hamcrest、AssertJ 或任何其他断言库,还可以使用许多其他断言。但是现在让我们从基本的 JUnit 断言开始。
assertTrue和assertFalse用于检查布尔条件的值。提供了assertFalse而不是assertTrue(!somethingYouExpectToReturnFalse)(例如assertTrue (5 < 6)和assertFalse (5>6))。
assertNull和assertNotNull检查对象是否为空(如assertNull(Calculator)或assertNotNull(Calculator))。
assertSame和assertNotSame检查这两个对象是对assertSame的同一个对象的引用还是对assertNotSame的同一个对象的引用。这与 equals 不同,equals 比较两个对象的值,而不是对象本身。
assertThat 可以像assertEquals一样使用,我们现在可以说assertThat(is(7), mCalculator.add(3, 4)),而不是说assertEquals(7, mCalculator.add(3,4), 0)。
失败仅仅是一个失败的测试,代表那些不应该被触及的代码,或者告诉你“这里有龙”
命令行
可以从命令行使用以下命令运行单元测试:gradlew test --continue.``gradlew任务运行单元测试,continue告诉gradlew如果任何单元测试失败,不要停止,这正是我们想要的。
C:\AndroidStudioProjects\BasicSample>gradlew test --continue
Downloadinghttps://services.gradle.org/distributions/gradle-2.2.1-all.zip
............................................................................
..................................................
Unzipping C:\Users\godfrey\.gradle\wrapper\dists\gradle-2.2.1-all\6dibv5rcnnqlfbq9klf8imrndn\gradle-2.2.1-all.zip to C:\Users\godfrey\.gradle\wrapper\dists\gradle-2.2.1-all\6dibv5rcnnqlfbq9klf8imrndn
Downloadhttps://jcenter.bintray.com/com/google/guava/guava/17.0/guava-17.0.jar
Downloadhttps://jcenter.bintray.com/com/android/tools/lint/lint-api/24.2.3/lint-api-24.2.3.jar
Downloadhttps://jcenter.bintray.com/org/ow2/asm/asm-analysis/5.0.3/asm-analysis-5.0.3.jar
Downloadhttps://jcenter.bintray.com/com/android/tools/external/lombok/lombok-ast/0.2.3/lombok-ast-0.2.3.jar
:app:preBuild UP-TO-DATE
:app:preDebugBuild UP-TO-DATE
:app:checkDebugManifest
:app:prepareDebugDependencies
:app:compileDebugAidl
:app:compileDebugRenderscript
.
.
.
:app:compileReleaseUnitTestSources
:app:assembleReleaseUnitTest
:app:testRelease
:app:test
BUILD SUCCESSFUL
Total time: 3 mins 57.013 secs
您可能希望从命令行运行您的测试,尤其是第一次运行单元测试时,使用gradlew test --continue命令以便您可以看到发生了什么,或者打开 Android Studio 中的 gradle 控制台。否则你可能会奇怪为什么 Android Studio 下载了运行单元测试所需的所有文件却什么也没发生。
如果您使用持续集成构建工具,如 Jenkins,命令行测试执行也非常有用。
JUnit 选项
JUnit4 有以下注释
@Before@After@Test@BeforeClass@AfterClass@Test(timeout=ms)
@Test用于注释所有的测试方法(见清单 2-1 ,没有它,方法将不能作为测试运行。@Test(timeout=ms)是标准标注上的一点小皱纹;如果测试花费的时间超过了以毫秒为单位定义的超时时间,它只是说放弃。
Listing 2-1. @Test Method
@Test
public void calculator_CorrectSub_ReturnsTrue() {
assertEquals(1, mCalculator.sub(4, 3),0);
}
@Before和@After用于您需要的任何设置和拆卸功能。例如,@Before可以包含代码来写入日志文件,或者创建测试中使用的对象,或者打开数据库,然后用测试数据播种数据库。@After通常用于撤销任何这些@Before变更,比如删除数据库中的测试行,等等(参见清单 2-2 )。
Listing 2-2. @Before and @After Annotations
public class CalculatorTest {
private Calculator mCalculator;
@Before
public void setUp() {
mCalculator = new Calculator();
}
@Test
public void calculator_CorrectAdd_ReturnsTrue() {
assertEquals(7, mCalculator.add(3, 4),0);
}
@Test
public void calculator_CorrectSub_ReturnsTrue() {
assertEquals(1, mCalculator.sub(4, 3),0);
}
@Test
public void calculator_CorrectMul_ReturnsTrue() {
assertEquals(12, mCalculator.mul(3, 4),0);
}
@Test
public void calculator_CorrectDiv_ReturnsTrue() {
assertEquals(3, mCalculator.div(12, 4),0);
}
@After
public void tearDown() {
mCalculator = null;
}
}
@Before和@After在每次测试前被调用,但是如果你想在所有测试前和所有测试后分别改变一次设置,那么你应该使用@BeforeClass和@AfterClass。setUp方法现在是setUpBeforeClass而不是setUpBeforeTest。在我们下面的@BeforeClass示例中,setUp和tearDown方法现在被声明为公共静态。Calculator被定义为静态的(参见清单 2-3 ,所以现在只有一个Calculator的实例,而不是每个测试一个。
Listing 2-3. Using @BeforeClass Annotation Instead of @Before
private static Calculator mCalculator;
@BeforeClass
public static void setUp() {
mCalculator = new Calculator();
}
HTML 输出
JUnit 在<path_to_your_project>/app/build/test-results/debug目录中输出 HTML 和 XML 风格的报告。当您试图准确跟踪一个或多个类何时开始失败,或者某个包或类比其他包或类更容易失败时,这些报告主要用作参考(见图 2-1 )。
图 2-1。
HTML reporting
如果您需要将结果导入到另一个工具中,同一目录中还有一个 XML 输出。
分组测试
随着你的单元测试的增长,根据它们需要的时间将它们分成小型、中型或大型测试并不是一个坏主意。当您编码时,编写和执行单元测试应该非常快,但是可能有更全面的测试,您可能希望每天运行一次或者在构建被签入时运行。
图 2-2 摘自一个旧的谷歌测试博客(见 http://googletesting.blogspot.com/2010/12/test-sizes.html ),它很好地展示了什么时候你应该将你的测试分成中型或大型测试,这样它们就不会减慢开发过程。
图 2-2。
Grouping unit tests into categories
小型测试将是普通的基于方法的单元测试,带有模拟的数据库或网络访问(稍后将详细介绍)。因为 Espresso 测试需要模拟器或设备来运行,所以它们会自动被视为中型或大型测试。
清单 2-4 展示了用必要的import语句标注测试是小型还是中型的正常方式。
Listing 2-4. Classic Unit Testing Grouping
导入 Android . test . suite builder . annotation . small test;
导入 Android . test . suite builder . annotation . medium test;
@SmallTest
public void calculator_CorrectAdd_ReturnsTrue() {
assertEquals(mCalculator.add(3, 4),7,0);
}
@SmallTest
public void calculator_CorrectSub_ReturnsTrue() {
assertEquals(mCalculator.sub(4, 3),1,0);
}
@MediumTest
public void calculator_CorrectMul_ReturnsTrue() {
assertEquals(mCalculator.mul(3, 4),12,0);
}
@MediumTest
public void calculator_CorrectDiv_ReturnsTrue() {
assertEquals(mCalculator.div(12, 4),3,0);
}
参数化测试
如果我们想测试我们的计算器,我们将不得不做更多的测试,而不仅仅是数字 3 和 4 的加、减、乘、除组合。清单 2-5 还有一些测试,让我们对我们的实现更有信心。运行测试,他们都通过了。
Listing 2-5. Adding More Test Conditions
@Test
public void calculator_CorrectAdd_ReturnsTrue() {
assertEquals``(7,``mCalculator
assertEquals``(7,``mCalculator
assertEquals``(10,``mCalculator
assertEquals``(3,``mCalculator
assertEquals``(3260,``mCalculator
}
如果你正在编写单元测试,我猜你总是在寻找编写更好代码的方法,你会认为清单 2-5 中的代码很糟糕。所有那些硬编码看起来都不对,即使是测试代码。我们可以使用 JUnit 的参数化测试来解决这个问题。
重构代码以添加参数化测试,如下所示:
- 在类的顶部添加
@RunWith(Parameterized.class),告诉编译器我们正在使用参数进行测试 - 添加
import语句,import static org.junit.runners.Parameterized.Parameters; - 创建您的测试参数集合,在本例中是
operandOne、operandTwo和expectedResult - 添加该类的构造函数
- 使用这些参数来支持您的测试
清单 2-6 显示了完整的代码。为了简单起见,我们将代码转换为只处理整数。
Listing 2-6. Paramaterized Testing Example
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertEquals;
import static org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.``class
public class CalculatorParamTest {
private int mOperandOne ;
private int mOperandTwo ;
private int mExpectedResult ;
private``Calculator``mCalculator
/* Array of tests */
@Parameters
public static Collection<Object[]> data() {
return Arrays. asList ( new
{3, 4, 7},
{4, 3, 7},
{8, 2, 10},
{-1, 4, 3},
{3256, 4, 3260}
});
}
/* Constructor */
public CalculatorParamTest( int mOperandOne, int mOperandTwo, int mExpectedResult) {
this``.``mOperandOne
this``.``mOperandTwo
this``.``mExpectedResult
}
@Before
public void setUp() {
mCalculator``=``new
}
@Test
public void testAdd_TwoNumbers() {
int resultAdd = mCalculator .add( mOperandOne , mOperandTwo );
assertEquals(resultAdd,``mExpectedResult
}
}
当代码运行时,我们在统计框架中得到以下结果(参见图 2-3 )。
图 2-3。
Parameterized test results
摘要
在这一章中,我们更详细地研究了单元测试。在下一章中,我们将会看到一些你想要添加到你的单元测试工具带上的第三方工具。在本书的后面,我们将回到单元测试,展示如何在 TDD(测试驱动开发)环境中编写单元测试。
三、第三方工具
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-9701-8_3) contains supplementary material, which is available to authorized users.
JUnit 本身可能就是您所需要的,但是您可以将许多优秀的第三方工具附加到 JUnit 上,让您的 Android 测试大放异彩。
在本章中,我们将了解以下工具:
- Hamcrest 寻找更好的断言
- 这样我们可以测量我们的 JUnit 代码覆盖率
- 这样我们就可以将单元测试集中在代码上
- 这样我们就可以测试我们的机器人活动
- Jenkins 让我们的测试自动化
哈姆克雷斯特断言
除了简单的 Hello,World-type 应用可能还需要比 JUnit 4.x 更好的断言。它还提供了更多的灵活性,允许您现在包括范围,而不仅仅是单个值。正如 Hamcrest 文档所说,Hamcrest 允许您创建“可以组合起来创建灵活的意图表达的匹配器”表 3-1 列出了大多数可用的 Hamcrest 断言,您也可以编写自己的断言。
表 3-1。
Hamcrest Assertions
| 包裹 | 断言 | | --- | --- | | 核心匹配者 | `allOf, any, anyOf, anything, array, both, containsString, describedAs, either, endsWith, equalTo, everyItem, hasItem, hasItems, instanceOf, is, isA, not, notNullValue, nullValue, sameInstance, startsWith, theInstance` | | 匹配项 | `allOf, any, anyOf, anything, array, arrayContaining, arrayContainingInAnyOrder, arrayWithSize, both, closeTo, comparesEqualTo, contains, containsInAnyOrder, containsString, describedAs, either, empty, emptyArray, emptyCollectionOf, emptyIterable, emptyIterableOf, endsWith, equalTo, equalToIgnoringCase, equaltToIgnoringWhiteSpace, eventFrom, everyItem, greaterThan, greaterThanOrEqualTo, hasItem, hasItemInArray, hasItems, hasKey, hasProperty, hasSize, hasToString, hasValue, hasXPath, instanceOf, is, isA,isEmptyOrNullString, isIn, isOneOf, iterableWithSize, lessThan, lessThanOrEqualTo, not, notNullValue, nullValue, sameInstance, samePropertyValueAs, startsWith, stringContainsInOrder, theInstance, typeCompatibleWith` | | 情况 | `and, matched, matching, notMatched, then` | | 火柴插入 | `assertThat` |清单 3-1 展示了如何将 Hamcrest 库添加到您的build.gradle文件中,以便在您的应用中包含 Hamcrest 功能。记得点击“立即同步”按钮。
Listing 3-1. Adding Hamcrest Library Dependency
dependencies {
testCompile ’junit:junit:4.12’
testCompile ’org.hamcrest:hamcrest-library:1.3’
}
现在我们重构我们的测试,让它们读起来更像英语(见清单 3-2 )。
Listing 3-2. Hamcrest Assertions
@Test
public void calculator_CorrectHamAdd_ReturnsTrue() {
assertThat("Calculator cannot add 3 plus 4", is(7), mCalculator.add(3, 4));
}
我们还可以使用greaterThan和LessThan断言向我们的测试添加范围(参见清单 3-3 )。
Listing 3-3. greaterThan and lessThan
public void calculator_CorrectHamAdd_ReturnsTrue() {
assertThat("Greater than failed", greaterThan(6), mCalculator.add(3, 4));
assertThat("Less than failed", lessThan(8), mCalculator.add(3, 4));
}
或者,我们可以使用both命令将两者结合起来(参见清单 3-4 )。
Listing 3-4. Using the both Matcher
@Test
public void calculator_CorrectHamAdd_ReturnsTrue() {
assertThat("Number is out of range", both(greaterThan(6)).and(lessThan(8)), mCalculator.add(3, 4),);
}
我们只是触及了 matchers 的皮毛,但毫无疑问,您可以看到 Hamcrest 可以让我们的测试变得多么强大。
杰柯
单元测试需要某种形式的代码覆盖来找到代码中任何未测试的部分。代码覆盖工具输出代码度量报告和带注释的代码,以显示哪些代码已经过单元测试(绿色),哪些没有被单元测试覆盖(红色)。图 3-1 显示了取自eclemma.org网站的 JaCoCo 的代码覆盖率数据。
图 3-1。
Code coverage example
代码覆盖率度量测量了多少源代码已经过单元测试。就我个人而言,我不太相信在 Android 项目中有一个代码覆盖度量目标;它应该作为一个指南,而不是一个强制性的要求。然而,如果一个项目只有 5%的代码覆盖率,那么你就不是在真正地进行单元测试,而只是口头上支持这项技术。
Android Studio 将调用 JaCoCo 来完成单元测试的代码覆盖报告,但是您需要执行以下任务:
- 在
build.gradle文件中设置testCoverageEnabled为真 - 将代码覆盖率运行器更改为 JaCoCo
- 使用代码覆盖率运行单元测试
- 查看代码覆盖率
要在 Android 项目中包含代码覆盖率,在build.gradle文件中的调试buildTypes中将testCoverageEnabled设置为 true(参见清单 3-5 ,并在做出更改后点击 Sync now。
Listing 3-5. build.gradle JaCoCo Changes
buildTypes {
debug {
testCoverageEnabled true
}
}
要编辑配置,转到运行➤编辑配置(见图 3-2 )。
图 3-2。
Choose Edit Configurations
点击 Code Coverage 选项卡,并将 coverage runner 更改为 JaCoCo(参见图 3-3 )。
图 3-3。
Changing coverage runner
通过右键单击方法并选择 Run CalculatorTest with Coverage 来运行测试(参见图 3-4 )。
图 3-4。
Run Calculator Test with Coverage
代码覆盖率报告显示在覆盖率选项卡中(见图 3-5 ,在这里你可以看到我们的Calculator方法有 50%的代码覆盖率。
图 3-5。
Code coverage tests
方法中显示了红色/绿色的代码覆盖率,尽管可能很难看到(参见图 3-6 )。Android Studio 中的代码覆盖集成是新的。毫无疑问,在未来的版本中会更容易看到红/绿覆盖。
图 3-6。
Code coverage
莫基托
在第二章的“分组测试”部分,我们讨论了小型、中型和大型测试。实际上,单元测试应该总是小测试。但是如果我们进行网络连接或者从文件系统或数据库中读取数据,那么根据定义,我们不是在执行小单元测试。我们还假设第三方 web 服务或数据库可能不会在我们每次运行测试时运行。因此,最坏的情况是,我们的测试将会失败,但原因是错误的(例如,网络中断)。我们使用模仿框架来模仿任何与外部资源对话的代码,并将我们所有的单元测试返回给更小的团队。Mockito 与 Android Studio 配合得非常好,所以我们将在本章和后续章节中使用该工具。
清单 3-6 展示了如何通过包含 testCompile ’org.mockito:mockito-core:1.10.19’库来将 Mockito 库添加到您的build.gradle文件中。再次记住,完成后点击“立即同步”链接。
Listing 3-6. Adding Mockito Library
dependencies {
testCompile ’junit:junit:4.12’
testCompile ’org.hamcrest:hamcrest-library:1.3’
testCompile ’org.mockito:mockito-core:1.10.19’
}
谷歌的 Android 样本有一个名为 NetworkConnect 的网络应用,你可以在 https://github.com/googlesamples/android-NetworkConnect 找到它。图 3-7 显示了返回谷歌网页 HTML 的应用的基本功能。
图 3-7。
NetworkConnect app
在模拟代码之前,我们需要将网络访问代码剪切并粘贴到它自己的类中(参见清单 3-7 ,我们称之为DownloadUrl)。
Listing 3-7. DownloadUrl Code
public class DownloadUrl {
public String loadFromNetwork(String urlString) throws IOException {
InputStream stream = null;
String str ="";
try {
stream = downloadUrl(urlString);
str = readIt(stream, 88);
} finally {
if (stream != null) {
stream.close();
}
}
return str;
}
public InputStream downloadUrl(String urlString) throws IOException {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(10000 /* milliseconds */);
conn.setConnectTimeout(15000 /* milliseconds */);
conn.setRequestMethod("GET");
conn.setDoInput(true);
conn.connect();
InputStream stream = conn.getInputStream();
return stream;
}
public String readIt(InputStream stream, int len) throws IOException, UnsupportedEncodingException {
Reader reader = null;
reader = new InputStreamReader(stream, "UTF-8");
char[] buffer = new char[len];
reader.read(buffer);
return new String(buffer);
}
}
MainActivity现在如下调用DownloadUrl(参见清单 3-8 )。
Listing 3-8. Updated NetworkConnect MainActivity Code
private class DownloadTask extends AsyncTask<String, Void, String> {
DownloadUrl htmlStr = new DownloadUrl();
@Override
protected String doInBackground(String… urls) {
try {
return htmlStr.loadFromNetwork(urls[0]);
} catch (IOException e) {
return getString(R.string.connection_error);
}
}
/**
* Uses the logging framework to display the output of the fetch
* operation in the log fragment.
*/
@Override
protected void onPostExecute(String result) {
Log.i(TAG, result);
}
}
我们现在可以编写一个单元测试,看看DownloadUrl代码是否在我们的单元测试中返回 HTML(参见清单 3-9 )。
Listing 3-9. Network Connect Unit Test
public class DownloadUrlTest {
DownloadUrl tDownloadUrl;
String htmlStr;
@Before
public void setUp() {
try {
htmlStr = tDownloadUrl.loadFromNetwork("http://www.google.com
} catch (IOException e) {
// network error
}
}
@Test
public void downloadUrlTest_ReturnsTrue() {
assertThat(htmlStr,containsString("doctype"));
}
}
因为我们正在进行网络调用,所以我们应该使用 Mockito 模拟网络访问。对于这个例子,我们只需要做几件事来模拟 web 服务器调用。首先模拟这个类,这样 Mockito 就知道它需要替换什么功能。接下来,告诉 Mockito 当使用Mockito.when().thenReturn()格式调用您正在测试的方法时要返回什么,格式如下:Mockito.when(tDownloadUrl.loadFromNetwork(" http://www.google.com ")。然后回车("<!doctype html><html itemscope=\"\" itemtype=\" http://schema.org/WebPage\ " lang=\"en\"><head>");。
现在,当进行loadFromNetwork调用时,它将返回我们的部分网页,而不是 www.google.com 网页的实际 HTML(参见清单 3-10 )。您可以通过打开和关闭网络访问来测试这一点。
Listing 3-10. Mocked Network Access
@RunWith(MockitoJUnitRunner.class)
public class DownloadUrlTest {
public DownloadUrl tDownloadUrl = Mockito.mock(DownloadUrl.class);
@Before
public void setUp() {
try {
Mockito.when(tDownloadUrl.loadFromNetwork(" http://www.google.com”)。然后返回("/root > html><html itemscope=\"\" itemtype=\" http://schema.org/WebPage\ " lang=\"en\"><head>");
} catch (IOException e) {
// network error
}
}
@Test
public void downloadUrlTest_ReturnsTrue() {
try {
assertThat(tDownloadUrl.loadFromNetwork("http://www.google.com
} catch (IOException e) {
//
}
}
}
我们将在下一章回到 Mockito,并向您展示如何模拟数据库和共享首选项访问,以及如何使用其他工具来扩展 Mockito 功能,以帮助分离或分离您的代码。
机器人电器
除非测试安卓活动,否则无法测试安卓应用。你可以使用像 Mockito 和 JUnit 这样的工具来测试它,但是如果你不测试它的活动,你就错过了应用的一个关键元素。如果你不测试这些活动向你的用户显示了什么,你就不能确定你的应用显示了正确的信息。使用模拟器测试框架,如 Espresso 或葫芦,这相对容易。但是,如果我们使用 Robolectric,我们也可以在没有仿真器的情况下测试它。
要安装 Robolectric 3.0,请将以下依赖项添加到 build.gradle 文件中(参见清单 3-11 )。
Listing 3-11. Adding Robolectric Library Dependency
dependencies {
testCompile ’junit:junit:4.12’
testCompile ’org.robolectric:robolectric:3.0’
}
您还需要对您的应用配置进行更改。转到运行-编辑配置,如果你在 Mac 或 Linux 上运行,然后将工作目录更改为$MODULE_DIR$,或者如果你在 Windows 机器上运行,在工作目录的末尾添加一个\app(参见图 3-8 )。
图 3-8。
Robolectric Working Directory fix
清单 3-12 显示了一个使用 Robolectric 测试 Hello World 显示在MainActivity上的单元测试。请注意将目标 SDK 设置为 API 21 并告诉 Robolectric 在哪里可以找到AndroidManifest.xml文件的配置信息。
Listing 3-12. Robolectric Hello World
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
public class RobolectricUnitTest {
@Test
public void shouldHaveHappySmiles() throws Exception {
String hello = new MainActivity().getResources().getString(R.string.hello_world);
assertThat(hello, equalTo("Hello world!"));
}
}
以与运行单元测试相同的方式运行测试,方法是右键单击测试类名并选择“运行 RobolectricTest”。不需要仿真器就可以通过测试。相对而言,Robolectric 测试比 JUnit4 测试耗时更长,但仍比模拟器测试快得多。
图 3-9。
Robolectric Hello World test passes
詹金斯
转向敏捷过程会产生相当大的开销。令人欣慰的是,我们不再需要担心模拟器花这么长时间来启动普通的 JUnit 测试。现在需要几秒钟而不是几分钟。然而,随着应用的增长,相应的单元测试数量也在增长,最终手动运行测试将需要时间。正确构建和测试应用的步骤也将开始变得更加复杂。由于人类不擅长繁琐的多步骤过程,因此使用持续集成(CI)服务器来尽可能地自动化该过程以减少任何不必要的测试错误是有意义的。
出于我们的目的,我们将使用 Jenkins,因为它有如此多的可用插件。但是,如果您更熟悉这些 CI 环境,还有许多其他选项,如 Travis、TeamCity 或 Bamboo,也可以同样很好地工作。
安装
从 http://jenkins-ci.org/ 下载 Jenkins 服务器。安装并转到http://localhost:8080,你应该会看到如图 3-10 所示的屏幕。
图 3-10。
Jenkins start-up screen
配置 Jenkins
为了让它在我们的 Android 环境中有用,我们需要添加一些插件。点击管理詹金斯➤管理插件(见图 3-10 ),搜索并添加 Gradle 和 GIT 插件或您使用的任何其他源代码管理系统。当你完成后,你安装的插件屏幕应该看起来如图 3-11 所示。
图 3-11。
Installed plug-ins
接下来我们需要配置 Jenkins,这样它就知道你在哪里安装了 Android。点击管理詹金斯➤配置系统,向下滚动到全局属性,点击环境变量复选框,输入安装 Android 的目录ANDROID_HOME(见图 3-12 )。
图 3-12。
Setting Environment variables
创建自动化作业
现在我们已经配置了 Jenkins,我们需要创建我们的第一个自动化作业。返回仪表板,点击创建新工作(图 3-10 )。输入您的项目名称并选择自由式项目(参见图 3-13 )。
图 3-13。
Creating a new item
我们需要告诉詹金斯哪里能找到密码。在这个例子中,我们使用 Git 作为我们的源代码管理系统。这里我们再次使用 Google NetworkConnect 示例。输入 Git 存储库 URL。因为是公开回购,没有凭证,所以我们要跳过这一步。也只有一个分支,所以我们可以把分支说明符留为master(见图 3-14 )。
图 3-14。
Enter Network Connect repository details
向下滚动到构建部分并选择调用 Gradle 脚本(参见图 3-15 )。
图 3-15。
Invoke Gradle script
在构建步骤中,选择使用 Gradle Wrapper,选中使 gradlew 可执行并从根构建脚本目录。在开关部分输入--refresh-dependencies和--profile。在这种情况下,在 Tasks 部分输入 assemble。点击保存(见图 3-16 )。
图 3-16。
Configure the Build
现在我们已经准备好构建我们的应用了。点击项目页面上的立即构建(见图 3-17 )。
图 3-17。
Project page
一旦构建开始,您将看到一个进度指示器,以了解您的任务进展如何。如果你想知道发生了什么,点击构件号(见图 3-18 )。
图 3-18。
View Build progress
现在点击控制台输出,你可以看到发生了什么,就像你从命令行运行应用一样(见图 3-19 )。
图 3-19。
Click Console Output
在我们的示例中,没有错误,构建成功(参见图 3-20 )。如果不是这种情况,那么控制台输出页面对于查看失败的原因非常有帮助。
图 3-20。
Console Output
我们将在本书的后面使用 Jenkins 来自动化我们的 JUnit 和 Espresso 测试。
摘要
在这一章中,我们已经看到了一些工具,我们将在整本书中使用它们来使我们的测试更加有效和高效。在最近的过去,让这个堆栈运行起来是一件非常令人沮丧的任务,但幸运的是,现在不再是这样了。
四、模拟
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-9701-8_4) contains supplementary material, which is available to authorized users.
无论是否在 Android 平台上,主要目标之一是隔离我们正在测试的代码。当我们编写测试时,我们希望测试特定类的方法,而不与应用中的其他类或任何外部元素(如 web 服务)进行任何相关的交互。我们应该测试一个单独的方法,而不是它的依赖项,并且这个方法应该是测试覆盖的唯一代码,其他的都被模拟了。
模拟这些第三方交互是一种很好的方式,可以帮助我们在方法周围设置围栏,这样我们在进行测试时就不依赖于网络或设备的位置或美国或英国时间。测试失败的唯一原因是因为代码有问题,而不是因为外部依赖(如 wifi)不起作用。
但是我们想要使用模拟框架还有另一个主要的特定于 Android 的原因,那是因为我们希望我们所有的测试都是可以在没有仿真器的情况下运行的测试。模仿依赖关系可以让您的测试运行速度比可怕的替代方法快几个数量级,后者是等待几分钟模拟器启动。当然有时你需要使用仿真器,比如当你测试活动时(见第五章),但是如果你不测试活动,mocking 给你信心将你的测试注释为@SmallTest而没有仿真器开销。
在这一章中,我们将看看如何使用 Mockito 来模拟以下测试隔离和更快测试执行的交互。
- 共享偏好
- 时间
- 设置
- SQLite 数据库
我们也已经在第二章中简要介绍了 web 服务。
共享偏好
共享偏好通常以 xml 文件的形式存储在设备的/data/data/<name of your package>文件夹中。正常情况下,任何需要文件系统访问的测试都意味着使用仿真器,除非我们使用 Mockito。
在我们的示例中,为了展示这是如何工作的,我们将使用一个简单的登录应用。除了让你用用户名、密码和电子邮件地址登录,然后在第二页上显示这些信息外,它没有做太多事情(见图 4-1 )。
图 4-1。
Registration app
在我们的假应用中,我们希望显示用户已经注册,因此用户第一次登录时,我们将写入应用的共享首选项。清单 4-1 显示了写入共享首选项文件的代码。该方法将活动和字符串作为其参数。完整的代码可在 Apress 网站的源代码/下载区获得。
Listing 4-1. Saving to the Shared Preferences
public void saveSharedPreferences(Activity activity, String spValue) {
SharedPreferences preferences = activity.getPreferences(Activity.MODE_PRIVATE);
preferences.edit().putString(SHAREDPREF, spValue).apply();
}
清单 4-2 显示了查看存储在我们共享首选项中的值的调用。
Listing 4-2. Reading from Shared Preferences
public String getSharedPreferences(Activity activity) {
SharedPreferences preferences = activity
.getPreferences(Activity.MODE_PRIVATE);
return preferences.getString(SHAREDPREF, "Not registered");
}
在 Android 模拟器上运行应用,并输入您的登录凭据。您可以通过在模拟器上运行adb shell命令来查看共享首选项中存储的内容(参见清单 4-3 )。它也可以在根设备上工作。
Listing 4-3. Login App’s Shared Preference
>adb shell
root@generic:/ # cd /data/data/com.riis.hellopreferences/shared_prefs
root@generic:/data/data/com.riis.hellopreferences/shared_prefs # ls
MainActivity.xml
root@generic:/data/data/com.riis.hellopreferences/shared_prefs # cat MainActivity.xml
<?xml version=’1.0’ encoding=’utf-8’ standalone=’yes’ ?>
<map>
<string name="registered">true</string>
</map>
共享首选项内置于 Android 功能中,这意味着我们不需要测试它。在一个真实的应用中,我们可能想要测试我们的代码,假设用户在应用被测试时已经注册。清单 4-4 展示了对getSharedPreferences方法sharedPreferencesTest_ReturnsTrue的模拟调用。
Listing 4-4. Mocked getSharedPreferences
// Annotation to tell compiler we’re using Mockito
@RunWith(MockitoJUnitRunner.class)
public class UserPreferencesTest {
// Use Mockito to initialize UserPreferences
public UserPreferences tUserPreferences = Mockito.mock(UserPreferences.class);
private Activity tActivity;
@Before
public void setUp() {
// setup the test infrastructure
// Use Mockito to declare the return value of getSharedPreferences()
Mockito.when(tUserPreferences.getSharedPreferences(tActivity)).thenReturn("true");
}
@Test
public void sharedPreferencesTest_ReturnsTrue() {
// Perform test
Assert.assertThat(tUserPreferences.getSharedPreferences(tActivity), is("true"));
}
}
总是返回 true,这样我们就可以绕过共享的偏好,继续我们测试中重要的事情。在本例中,我们修改了共享首选项代码,使其始终返回 true。主要是因为它从来没有真正运行过共享首选项代码。setup 块告诉 Mockito 您希望它如何运行,该类的模拟版本将按照指示运行,总是返回 true。
时间
利用接口可能是一种非常有用的模仿技术。例如,如果我们有一个Clock接口,它调用一个Clock实现类来告诉时间,那么我们使用 Mockito 模仿接口Clock类来提供我们自己的 Android 日期/时间环境。接口抽象允许我们隐藏实现,这样我们可以完全控制时区和一天中的时间,并创建更多的边缘案例测试来真正运行我们的代码。这是一个简单的“编码到接口”的例子。接口是我们在编写代码时试图满足的契约。然而,当测试实现时,接口可以与真实的实现、模拟的实现或者两者的组合进行对话。
清单 4-5 显示了Clock接口代码。
Listing 4-5. Clock Interface
import java.util.Date;
public interface Clock {
Date getDate();
}
清单 4-6 显示了Clock的实现代码。
Listing 4-6. ClockImpl Code
import java.util.Date;
public class ClockImpl implements Clock {
@Override
public Date getDate() {
return new Date();
}
}
这里的概念就像共享偏好一样。我们不必测试任何java.util.Date功能;我们只想测试我们编写的使用它的代码。清单 4-7 有几个简单的方法,以毫秒为单位将时间加倍到三倍。
Listing 4-7. Timechange Code
public class TimeChange {
private final Clock dateTime;
public TimeChange(final Clock dateTime) {
this.dateTime = dateTime;
}
public long getDoubleTime(){
return dateTime.getDate().getTime()*2;
}
public long getTripleTime(){
return dateTime.getDate().getTime()*3;
}
}
在我们的测试代码中(参见清单 4-8 ,我们模拟出Clock和java.util.Date类,这允许我们将时间设置为我们想要的任何值,并运行一些断言来确保我们的doubleTime和tripleTime方法按预期运行。
Listing 4-8. TimeChangeTest Code
// Tell Android we’re using Mockito
@RunWith(MockitoJUnitRunner.class)
public class TimeChangeTest {
private TimeChange timeChangeTest;
@Before
public void setUp() {
// Mock the Date class
final Date date = Mockito.mock(Date.class);
Mockito.when(date.getTime()).thenReturn(10L);
// Mock the Clock class interface final Clock dt = Mockito.mock(Clock.class);
Mockito.when(dt.getDate()).thenReturn(date);
timeChangeTest = new TimeChange(dt);
}
@Test
public void timeTest() {
final long doubleTime = timeChangeTest.getDoubleTime();
final long tripleTime = timeChangeTest.getTripleTime();
assertEquals(20, doubleTime);
assertEquals(30, triple Time);
}
}
系统属性
如果我们想避免使用模拟器进行测试,我们需要伪造任何 Java 或内置的 Android 功能。在大多数情况下,这正是我们正在寻找的;正如我们在前面的例子中看到的,我们没有测试共享偏好功能或日期功能。同样,我们也不想测试 Android 设置(比如音频管理器)。
我们的AudioHelper代码只有一个方法maximizeVolume。清单 4-9 显示了我们最大化音量的代码。
Listing 4-9. Testing the Max-Min Limits of Our Code
import android.media.AudioManager;
public class AudioHelper {
public void maximizeVolume(AudioManager audioManager) {
int max = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
audioManager.setStreamVolume(AudioManager.STREAM_RING, max, 0);
}
}
清单 4-10 显示了我们将铃声设置到最大音量的测试代码。
Listing 4-10. Max Volume Limits
/**
* Unit tests for the AudioManager logic.
*/
// Define the test as SmallTest for grouping tests
@SmallTest
public class AudioHelperTest {
private final int MAX_VOLUME = 100;
@Test
public void maximizeVolume_Maximizes_Volume() {
// Create a mockAudioManager object using Mockito
AudioManager audioManager = Mockito.mock(AudioManager.class);
// Inform Mockito what to return when audioManager.getStreamMaxVolume is called Mockito.when(audioManager.getStreamMaxVolume(AudioManager.STREAM_RING)).thenReturn(MAX_VOLUME);
// Run method we’re testing, passing mock AudioManager
new AudioHelper().maximizeVolume(audioManager);
//verify with Mockito that setStreamVolume to 100 was called.
Mockito.verify(audioManager).``setStreamVolume
}
}
我们创建 mock AudioManager对象,并告诉我们的测试代码在我们进行调用时返回MaxVolume,然后我们验证 Mockito 在进行调用时将音量设置为最大。
数据库ˌ资料库
共享首选项对于将参数、URL(统一资源定位器)或 API(应用编程接口)密钥存储到第三方库非常有用,但对于大量的表格数据就不那么好了。如果你想在手机上保存 Android 中的大量电子表格类型的数据,那么更常见的是使用 SQLite 数据库进行存储,因为它是免费的、轻量级的,并且可以处理数十到数千行数据。如果您需要升级到更大的数据集,那么您更有可能将它们存储在后端服务器上,而不是设备本身。
使用我们的示例应用(再次参见图 4-1 ,我们可以将用户名和电子邮件添加到 SQLite 数据库中。要写入 SQLite 数据库,你需要SQLHelper代码(参见清单 4-11 )。这是用于 Android SQLite 应用的典型样板代码。它创建并升级数据库及其表。在这种情况下,Users表中有一列是自动生成的 ID 以及用户名和电子邮件地址。
Listing 4-11. SQLite Code to Create User Database
public class SQLHelper extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "UserDb";
private static final String TABLE_USERS = "Users";
private static final String KEY_ID = "id";
private static final String KEY_FIRST_NAME = "firstName";
private static final String KEY_LAST_NAME = "lastName";
private static final String[] COLUMNS = {KEY_ID, KEY_FIRST_NAME, KEY_LAST_NAME};
public SQLHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String CREATE_USER_TABLE = "CREATE TABLE Users ( " +
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"firstName TEXT, "+
"lastName TEXT )";
db.execSQL(CREATE_USER_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS Users");
this.onCreate(db);
}
public void addUser(User user){
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(KEY_FIRST_NAME, user.getFirstName());
values.put(KEY_LAST_NAME, user.getLastName());
db.insert(TABLE_USERS, null, values);
db.close();
}
public User getUser(int id){
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query(TABLE_USERS, COLUMNS, " id = ?", new String[] { String.valueOf(id) }, null, null, null, null);
if (cursor != null) {
cursor.moveToFirst();
}
User user = new User();
user.setId(Integer.parseInt(cursor.getString(0)));
user.setFirstName(cursor.getString(1));
user.setLastName(cursor.getString(2));
return user;
}
}
}
过去,开发人员在测试期间通过使用内存中的 SQLite 数据库来隔离他们的数据库。您可以通过将DATABASE_NAME保留为空(即super(context, null, null, DATABASE_VERSION);)来实现这一点。不幸的是,这对我们不起作用,因为它仍然需要一个模拟器,所以我们将不得不依靠我们的模拟。
清单 4-12 显示了我们想要测试的UserOperations代码:这是我们的创建、读取、更新、删除(CRUD)代码。
Listing 4-12. CRUD Code for Our Database Calls
public class UserOperations {
private DataBaseWrapper dbHelper;
private String[] USER_TABLE_COLUMNS = { DataBaseWrapper.USER_ID, DataBaseWrapper.USER_NAME, DataBaseWrapper.USER_EMAIL };
private SQLiteDatabase database;
public UserOperations(Context context) {
dbHelper = new DataBaseWrapper(context);
}
public void open() throws SQLException {
database = dbHelper.getWritableDatabase();
}
public void close() {
dbHelper.close();
}
public User addUser(String name, String email) {
ContentValues values = new ContentValues();
values.put(DataBaseWrapper.USER_NAME, name);
values.put(DataBaseWrapper.USER_EMAIL, email);
long userId = database.insert(DataBaseWrapper.USERS, null, values);
Cursor cursor = database.query(DataBaseWrapper.USERS,
USER_TABLE_COLUMNS, DataBaseWrapper.USER_ID + " = "
+ userId, null, null, null, null);
cursor.moveToFirst();
}
public void deleteUser(User comment) {
long id = comment.getId();
database.delete(DataBaseWrapper.USERS, DataBaseWrapper.USER_ID
+ " = " + id, null);
}
public List getAllUsers() {
List users = new ArrayList();
Cursor cursor = database.query(DataBaseWrapper.USERS,
USER_TABLE_COLUMNS, null, null, null, null, null);
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
User user = parseUser(cursor);
users.add(user);
cursor.moveToNext();
}
cursor.close();
return users;
}
public String getUserEmailById(long id) {
User regUser = null;
String sql = "SELECT " + DataBaseWrapper.USER_EMAIL + " FROM " + DataBaseWrapper.USERS
+ " WHERE " + DataBaseWrapper.USER_ID + " = ?";
Cursor cursor = database.rawQuery(sql, new String[] { id + "" });
if (cursor.moveToNext()) {
return cursor.getString(0);
} else {
return "N/A";
}
}
private User parseUser(Cursor cursor) {
User user = new User();
user.setId((cursor.getInt(0)));
user.setName(cursor.getString(1));
return user;
}
}
在我们的测试中,我们将模拟一个addUser(name, email调用(参见清单 4-13 )。
Listing 4-13. testMockUser Code
/**
* Unit tests for the User Database class.
*/
@SmallTest
public class DatabaseTest {
private User joeSmith = new User("Joe", "Smith");
private final int USER_ID = 1;
@Test
public void testMockUser() {
//mock SQLHelper
SQLHelper dbHelper = Mockito.mock(SQLHelper.class);
//have mockito return joeSmith when calling dbHelper getUser
Mockito.when(dbHelper.getUser(USER_ID)).thenReturn(joeSmith);
//Assert joeSmith is returned by getUser
assertEquals(dbHelper.getUser(USER_ID), joeSmith);
}
}
在设置中,我们模拟出了dbHelper类以及底层的SQLiteDatabase。在testMockUser我们做了一个简单的测试呼叫,返回的用户是乔·史密斯。
詹金斯
在理想的环境中,我们希望在每次使用持续集成服务器(如 Jenkins)检入代码时自动运行测试,我们在第三章中讨论了这一点。
要在 Jenkins 中运行单元测试,单击添加构建步骤➤调用 Gradle 脚本并添加 testCompile 任务,如图 4-2 所示。
图 4-2。
Running unit tests in Jenkins
摘要
在这一章中,我们已经看了许多使用 Mockito 将我们的测试与任何底层 Android 和 Java 依赖隔离的场景。我们这样做的原因是为了确保我们只测试我们想要测试的代码,而不是任何与之交互的代码。您编写的代码都应该经过单元测试,包括模拟其依赖关系的交互的模拟。
本章的工作代码可以在 Apress 网站上找到。
五、Espresso
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-9701-8_5) contains supplementary material, which is available to authorized users.
除了简单的逻辑错误之外,Android 应用失败的原因还有很多。在最基本的情况下,该应用可能无法正确安装,或者当您从横向移动到纵向再返回时可能会出现问题。由于存在碎片,这种布局可能无法在您没有时间测试的任何数量的设备上工作,或者如果网络中断,它可能会挂起。
使用单元测试来测试这些条件是不可能的。我们将不得不使用另一个测试工具来测试我们的 GUI(图形用户界面)或活动。不幸的是,这也意味着我们又回到了使用设备和仿真器来进行测试。
有很多选择,比如 UIAutomator、葫芦、Robotium 和 Selenium。直到最近,我一直在使用 Calabash,因为它的 Given/When/Then 编写格式非常适合商业用户。然而,使用浓缩咖啡有显著的优势,这是很难抗拒的。
所有这些其他产品都是第三方产品,而 Espresso 是谷歌的第一方产品。通常这不会是任何一种优势,但是因为 Espresso 能够挂钩到 Android 生命周期中,它可以很好地准确知道活动何时准备好执行您的测试。Android 中的 GUI 测试通常充满了sleep()命令,以确保活动准备好接受您的数据。有了意式浓缩咖啡,你根本不需要等待或睡觉;它只是在应用准备好接受输入数据时触发测试。UI 线程和 Espresso 之间的同步意味着测试运行起来比使用其他工具更可靠。如果测试失败了,那是因为你的代码中有错误,而不是你需要给sleep()命令增加更多的时间。
onView
虽然我们已经在第一章中看到了意式浓缩咖啡,但还是有必要回到基础,做一个真正的 Hello,World Espresso 测试。
在第一章中,我们展示了如何设置 Espresso 环境,如下所示:
- 先决条件:安装 Android 支持库
- 在
build.gradle(app)文件中添加 Espresso 依赖 - 在构建变体中选择 Android 测试工具测试工件
- 在
src/androidTest/java文件夹中创建 GUI 测试 - 右键单击测试运行测试
Espresso 使用 OnView 格式,而不是 JUnit 或 Hamcrest 匹配器和断言。它有三个部分,即在我们测试的活动中查找元素的 ViewMatcher,执行操作(例如,click)的 ViewAction,以及确保文本匹配和测试通过的 ViewAssertion。
onView(ViewMatcher)
.perform(ViewAction)
.check(ViewAssertion);
你好世界
清单 5-1 显示了标准 Android Hello World 应用的代码。
Listing 5-1. Hello World
public class MainActivity extends Activity {
private TextView mLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
图 5-1 显示了在仿真器上运行的应用。我们简单的 Espresso 测试将找到文本,并确保它真的在说 Hello world!
图 5-1。
Hello world!
清单 5-2 显示了简单测试的代码。该测试被标注为@LargeTest,因为我们需要模拟器来运行 Espresso 测试。我们使用 JUnit4 规则来启动主活动(参见@Rule注释)。
一旦我们访问了活动,我们就使用 onView 代码来查找我们的 Hello World 文本和一个.check来查看该文本是否与在strings.xml文件中定义的一样。在这种情况下,不需要.perform步骤,所以省略了。
Listing 5-2. Hello World Espresso Test
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityTest {
@Rule
public ActivityTestRule<MainActivity> activityTestRule
= new ActivityTestRule<>(MainActivity.class);
@Test
public void helloWorldTest() {
onView(withId(R.id.hello_world))
.check(matches(withText(R.string.hello_world)));
}
}
测试通过,结果显示在 Android Studio 中,类似于单元测试输出(见图 5-2 )。
图 5-2。
Hello World Espresso test results
添加按钮
接下来,让我们在 Hello World 代码中添加一个按钮。为此,我们将清单 5-3 中的代码添加到我们的activity_main.xml文件中。字符串button_label也需要添加到strings.xml文件中。请注意,该按钮在默认情况下是启用的。
Listing 5-3. Adding Hello World Button
< Button
android:id="@+id/button"
android:text="@string/button_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
图 5-3 显示了我们修改后的带有新按钮的应用。
图 5-3。
Hello World with button
我们希望确保按钮处于打开或启用状态。清单 5-4 现在显示了测试代码。这次我们使用.perform动作来点击按钮。
Listing 5-4. onView Button Test
@Test
public void helloWorldButtonTest(){
onView(withId(R.id.button))
.perform(click())
.check(matches(isEnabled()));
}
当一切都是绿色时,测试成功运行(参见图 5-4 )。
图 5-4。
Hello World test results
视图匹配器
表 5-1 显示了可用的视图匹配器选项。
表 5-1。
ViewMatcher
| 种类 | 制榫机 | | --- | --- | | 用户属性 | `withId, withText, withTagKey, withTagValue, hasContentDescription, withContentDescription, withHint, withSpinnerText, hasLinks, hasEllipsizedText, hasMultilineTest` | | 用户界面属性 | `isDisplayed, isCompletelyDisplayed, isEnabled, hasFocus, isClickable, isChecked, isNotChecked, withEffectiveVisibility, isSelected` | | 对象匹配器 | `allOf, anyOf, is, not, endsWith, startsWith, instanceOf` | | 等级制度 | `withParent, withChild, hasDescendant, isDescendantOfA, hasSibling, isRoot` | | 投入 | `supportsInputMethods, hasIMEAction` | | 班级 | `isAssignableFrom, withClassName` | | 根匹配器 | `isFocusable, isTouchable, isDialog, withDecorView, isPlatformPopup` |视图操作
表 5-2 显示了可用的视图操作选项。
表 5-2。
ViewAction
| 种类 | 行动 | | --- | --- | | 点击/按下 | `click, doubleClick, longClick, pressBack, pressIMEActionButton, pressKey, pressMenuKey, closeSoftKeyboard, openLink` | | 手势 | `scrollTo, swipeLeft, swipeRight, swipeUp, swipeDown` | | 文本 | `clearText, typeText, typeTextIntoFocusedView, replaceText` |视图断言
表 5-3 显示了可用的视图断言选项。
表 5-3。
ViewAssertion
| 包裹 | 断言 | | --- | --- | | 布局断言 | `noEllipsizedText, noMultilineButtons, noOverlaps` | | 位置断言 | `isLeftOf, isRightOf, isLeftAllginedWith, isRightAlignedWith, isAbove, isBelow, isBottomAlignedWith, isTopAlignedWith` | | 其他的 | `matches, doesNotExist, selectedDescendentsMatch` |奏鸣曲
当我们使用任何 AdapterViews(如 ListView、GridView 或 Spinner)时,将无法找到数据。对于 AdapterViews,我们必须结合 onView 使用onData来定位和测试项目。
onData格式如下:
onData(ObjectMatcher)
.DataOptions
.perform(ViewAction)
.check(ViewAssertion)
可用的DataOptions有inAdapterView、atPosition或onChildView。
待办事项
为了了解这是如何工作的,让我们看看如何测试拥有 ListView 适配器的 ToDoList 应用(见图 5-5 )。
图 5-5。
ToDoList application
我们的应用使用 ListView 适配器。清单 5-5 显示了代码。
Listing 5-5. To Do List Code
public class MainActivity extends Activity {
private TextView mtxtSelectedItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mtxtSelectedItem = (TextView) findViewById(R.id.txt_selected_item);
String[] todolist = {"pick up the kids","pay bills","do laundry",
"buy groceries ","go the gym","clean room","call mum"};
List<String> list = Arrays.asList(todolist);
ArrayAdapter<String> adapter =
new ArrayAdapter(this, android.R.layout.simple_list_item_1, list);
ListView listView = (ListView) findViewById(R.id.list_of_todos);
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@ Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String text = ((TextView) view).getText().toString();
Toast.makeText(getApplicationContext(), text, Toast.LENGTH_LONG).show();
mtxtSelectedItem.setText(text);
}
});
}
}
确保一切正常的一个简单测试是在待办事项列表中选择一些事情,比如“去健身房”清单 5-6 显示了浓缩咖啡的代码。我们告诉测试查看onData代码中 AdapterView 中的位置[4],然后将它传递给 onView,这样它就可以检查文本是否确实说了“去健身房”
Listing 5-6. onData Test Code
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityTest {
@Rule
public ActivityTestRule<MainActivity> activityTestRule
= new ActivityTestRule<>(MainActivity.class);
@Test
public void toDoListTest(){
onData(anything())
.inAdapterView(withId(R.id.list_of_todos)).atPosition(4)
.perform(click());
onView(withId(R.id.txt_selected_item))
.check(matches(withText("go to the gym")));
}
}
使用模拟器或在设备上再次运行测试。
詹金斯
要在 Jenkins 中运行 Espresso 测试,请单击添加构建步骤➤调用 Gradle 脚本并添加 connectedCheck 任务(参见图 5-6 )。
图 5-6。
Adding Espresso tests in Jenkins
Espresso 需要一个模拟器来执行它的测试,所以您还需要安装 Android 模拟器插件。你可以选择让 Jenkins 使用现有的仿真器或者创建一个新的仿真器(见图 5-7 )。
图 5-7。
Using an existing emulator
摘要
在这一章中,我们已经研究了使用onView和onData的一些浓缩咖啡测试。最后,如果你想知道我们的测试套件中应该有多少 Espresso 测试,那么回到我们在第一章(图 1-1 )中的敏捷测试金字塔,你会看到我们应该总是有比 Espresso 测试更多的单元测试,或者换句话说,比@LargeTests更多的@SmallTests。