作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
马尔科·维塔斯的头像

Marko Vitas

拥有计算机科学学位的高级Android开发人员, Marko拥有6年以上的应用开发经验, 包括一个安全的手机银行应用程序.

以前在

Infinum
Share

As 经验丰富的应用开发者随着我们开发的应用程序的成熟,我们有一种直觉,是时候开始测试了. 业务规则通常意味着系统必须在不同的版本中提供稳定性. 理想情况下,我们还希望自动化构建过程并自动发布应用程序. 为此,我们需要适当的android测试工具,以确保构建按预期工作.

测试可以为我们构建的东西提供额外的信心. 构建一个完美的、没有bug的产品是很困难的(如果不是不可能的话). Therefore, 我们的目标是通过建立一个测试套件来快速发现应用程序中新引入的错误,从而提高我们在市场上取得成功的几率.

Android测试教程

说到Android, 以及各种各样的移动平台, 应用测试是一项挑战. 实现单元测试 遵循测试驱动开发或类似的原则通常会让人感觉不直观, 至少. 尽管如此,测试是重要的,不应该被视为理所当然或忽视. David, Kent and Martin 在一篇名为“Is TDD dead?”. 你也可以在那里找到实际的视频对话,并了解测试是否适合你的开发过程,以及你可以在多大程度上整合它, 从现在开始.

在这个Android测试教程中,我将引导您完成单元和验收, Android回归测试. 我们将重点关注Android上测试单元的抽象, 接下来是验收测试的示例, 专注于使过程尽可能快速和简单,以缩短开发人员- qa反馈周期.

我应该读吗??

本教程将探讨测试Android应用程序的不同可能性. 的当前测试可能性的开发人员或项目经理 Android平台 如果他们想采用本文中提到的任何方法,可以决定使用本教程吗. However, 这不是什么灵丹妙药, 由于涉及此类主题的讨论本质上因产品和截止日期而异, 代码库质量, 系统的耦合级别, 开发商对建筑设计的偏好, 要测试的特性的预计寿命, etc.

基于单位思考:Android测试

理想情况下,我们希望独立测试体系结构的一个逻辑单元/组件. 这样我们就可以保证我们的组件对于我们期望的一组输入正确地工作. 依赖性可以模拟,这将使我们能够编写执行速度快的测试. Furthermore, 我们将能够基于提供给测试的输入来模拟不同的系统状态, 在这个过程中涵盖了一些奇特的案例.

Android的目标 单元测试 是将程序的每个部分分离出来,并显示各个部分是正确的. 单元测试提供了代码段必须满足的严格的书面约定. 因此,它提供了几个好处. —Wikipedia

Robolectric

Robolectric 是一个Android单元测试框架,允许您在开发工作站的JVM内运行测试. robolelectric在Android SDK类被加载时重写它们,并使它们能够在常规JVM上运行, 从而提高测试时间. Furthermore, 它处理视图的膨胀, 资源加载, 以及更多在Android设备上用原生C代码实现的东西, 不再需要模拟器和物理设备来运行自动化测试.

Mockito

Mockito mock框架能让我们在Java中编写干净的测试吗. 它简化了创建测试双精度(模拟)的过程。, 哪些是用来取代生产中使用的组件/模块的原始依赖. StackOverflow回答讨论 关于模拟和存根之间的区别 在相当简单的术语,你可以阅读了解更多.

//你可以模拟具体类,而不仅仅是接口
 mockkedlist = mock(LinkedList.class);

//存根在实际执行之前出现
当(mockedList.get(0)).thenReturn(“第一”);

//下面打印"first"
System.out.println (mockedList.get(0));

//下面的输出为"null",因为get(999)没有存根
System.out.println (mockedList.get(999));

另外,使用Mockito,我们可以验证一个方法是否被调用:

//模拟创建
列表mockedList = mock(列表).class);

//使用模拟对象-它不会抛出任何“意外交互”异常
mockedList.add("one");
mockedList.clear();

//选择性的,显式的,高可读性的验证
验证(mockedList).add("one");
验证(mockedList).clear();

Testdroid

Now, 我们知道,我们可以指定动作-反应对来定义在模拟对象/组件上执行特定动作后发生的事情. Therefore, 我们可以模拟应用程序的整个模块, 对于每个测试用例,让模拟模块以不同的方式进行反应. 不同的方法将反映被测试组件和模拟组件对的可能状态.

单元测试

在本节中,我们将假设MVP (Model View Presenter)架构. 活动和片段是视图, 模型是调用数据库或远程服务的存储库层, 呈现者是“大脑”,它将所有这些结合在一起,实现控制视图的特定逻辑, models, 以及通过应用程序的数据流.

抽象组件

模拟视图和模型

在这个Android测试示例中, 我们将模拟视图, models, 以及存储库组件, 我们将对演示者进行单元测试. 这是最小的测试之一,针对体系结构中的单个组件. 此外,我们将使用方法存根来建立一个适当的、可测试的反应链:

@RunWith (RobolectricTestRunner.class)
@Config(manifest = "app/src/main/AndroidManifest "., emulateSdk = 18)
FitnessListPresenterTest {

	private Calendar cal =日历.getInstance ();

	@Mock
	private IFitnessListModel模型;

	@Mock
	private IFitnessListView视图;

	private IFitnessListPresenter;

	@Before
	Public void setup() {
		MockitoAnnotations.initMocks(这个);

		finnessentry . finnessentry . final.class);

		presenter = new FitnessListPresenter(view, model);
		/*
			定义期望的行为.

			在“doAnswer”中为“when”执行操作排队.
			为动作设置反应的清晰和同步的方式(存根).
			*/
		doAnswer((new Answer() {
			@Override
			public Object answer(InvocationOnMock调用)抛出Throwable {
				ArrayList items = new ArrayList<>();
				items.添加(entryMock);

				((IFitnessListPresenterCallback)主持人).onFetchAllSuccess(项目);
				返回null;
			}
		})).当(模型).fetchAllItems ((IFitnessListPresenterCallback)主持人);
	}

	/**
		验证模型.fetchItems被调用一次.
		验证视图.onFetchSuccess使用FitnessEntry类型的指定列表调用一次

		((ifitnesslistpresentcallback) presenter)的具体实现.onFetchAllSuccess(项目); 
		调用视图.onFetchSuccess (...) method. 这就是我们验证这个观点的原因.onFetchSuccess被调用一次.
	*/
	@Test
	公共无效testFetchAll() {
		presenter.fetchAllItems(假);
		//只能在模拟对象上调用verify
		验证(模型,乘以(1)).fetchAllItems ((IFitnessListPresenterCallback)主持人);
		验证(视图,乘以(1)).onFetchSuccess (new ArrayList<>(anyListOf(FitnessEntry.class)));
	}
}


用MockWebServer模拟全局网络层

能够模拟全局网络层通常是很方便的. MockWebServer允许我们对在测试中执行的特定请求的响应进行排队. 这使我们有机会模拟我们期望从服务器得到的模糊响应, 但并不容易繁殖. 它允许我们在编写少量额外代码的同时确保完全覆盖.

MockWebServer的代码存储库 提供一个简洁的示例,您可以参考该示例以更好地理解此库.

自定义测试双打

您可以编写自己的模型或存储库组件,并通过使用Dagger (http://square)为对象图提供不同的模块将其注入测试中.github.io/dagger/). 我们可以根据模拟模型组件提供的数据来检查视图状态是否被正确更新:

/**
	自定义模拟模型类 
*/
FitnessListErrorTestModel扩展FitnessListModel

	// ...

	@Override
	public void fetchAllItems(IFitnessListPresenterCallback) {
		callback.onError();
	}

	@Override
	public void fetchetemsinrange(最终IFitnessListPresenterCallback回调,DateFilter过滤器){
		callback.onError();
	}

}
@RunWith (RobolectricTestRunner.class)
@Config(manifest = "app/src/main/AndroidManifest "., emulateSdk = 18)
FitnessListPresenterDaggerTest {

    私人健身活动活动;
    FitnessListFragment;

    @Before
    Public void setup() {
        /*
            setupActivity在指定的类上运行Activity生命周期方法
        */
        机器人电动的.setupActivity (FitnessActivity.class);
        fitnessListFragment = activity.getFitnessListFragment ();
        
        /*
            用TestModule创建objectGraph
        */
        ObjectGraph localGraph = ObjectGraph.创建(TestModule.newInstance (fitnessListFragment));
        /*
            Injection
        */
        localGraph.注入(fitnessListFragment);
        localGraph.注入(fitnessListFragment.getPresenter ());
    }

    @Test
    testInteractorError() {
        fitnessListFragment.getPresenter ().fetchAllItems(假);

        /*
            假设我们的视图在报告错误时显示一条带有下面指定文本的Toast消息, 所以我们检查一下.
        */
        assertequal (ShadowToast.getTextOfLatestToast(), "出错了!");
    }

    @Module(
            注入= {
                    FitnessListFragment.class,
                    FitnessListPresenter.class
            },覆盖= true;
            Library = true
    )
    静态类TestModule {
        private IFitnessListView视图;

        private TestModule(IFitnessListView视图){
            this.View =视图;
        }

        公共静态TestModule newInstance(IFitnessListView视图){
            return new TestModule(view);
        }

        @Provides
        public IFitnessListInteractor ()
            返回新的FitnessListErrorTestModel();
        }

        @提供公共IFitnessListPresenter provideFitnessPresenter(){
            返回新的FitnessListPresenter(view);
        }
    }

}

运行测试

Android工作室

您可以轻松地右键单击测试类, method, 或整个测试包,并从IDE中的选项对话框中运行测试.

Terminal

从终端运行Android应用测试会在目标模块的“build”文件夹中为被测试的类创建报告. 更重要的是,如果您计划设置一个自动化的构建过程,您将使用终端方法. 在Gradle中,你可以通过执行以下命令来运行所有调试测试:

gradle testDebug

从Android工作室版本访问源代码集“测试”

Version 1.Android工作室 1和Android Gradle插件提供了对代码单元测试的支持. 你可以通过阅读他们的 关于它的优秀文档. 这个功能是实验性的, 而且这也是一个很好的包含,因为您现在可以从IDE轻松地在单元测试和插装测试源集之间切换. 它的行为方式与您在IDE中切换口味的方式相同.

Android单元测试

简化流程

编写Android应用程序测试可能没有开发原始应用程序那么有趣. Hence, 一些关于如何简化编写测试的过程和在设置项目时避免常见问题的技巧将大有帮助.

AssertJ安卓

AssertJ安卓, 你可能从名字就猜到了, 是一组辅助功能,是建立与Android的思想. 它是流行库的扩展 AssertJ. AssertJ安卓提供的功能范围从简单的断言,如“assertThat(view)”.isGone()”,对于复杂的事情,如:

为了(布局).isVisible()
    .isVertical ()
    .hasChildCount (4)
    .hasShowDividers (SHOW_DIVIDERS_MIDDLE)

使用AssertJ安卓及其可扩展性, 你可以保证一个简单的, 为Android应用程序编写测试的良好起点.

机器人电气和显化路径

而使用robolelectric, 您可能注意到您必须指定舱单位置, 且SDK版本设置为18. 你可以通过包含一个“Config”注释来做到这一点.

@Config(manifest = "app/src/main/AndroidManifest "., emulateSdk = 18)

在终端运行需要robolelectric的测试可能会带来新的挑战. 例如,您可能会看到像“主题未设置”这样的异常。. 测试是否在IDE中正确执行, 但不是在候机楼, 您可能正在尝试从无法解析指定清单路径的终端中的路径运行它. 清单路径的硬编码配置值可能没有从命令执行点指向正确的位置. 这可以通过使用自定义运行器来解决:

RobolectricGradleTestRunner扩展RobolectricTestRunner {
	public RobolectricGradleTestRunner(Class testClass) throws InitializationError {
		超级(testClass);
	}

	@Override
	getAppManifest(Config Config) {
		String appRoot = "../ app / src / main /”;
		字符串manifest路径= appRoot + "AndroidManifest ".xml";
		String resDir = appRoot + "res";
		String assetsDir = appRoot + "assets";
		AndroidManifest manifest = createAppManifest.fileFromPath (manifestPath),
		    Fs.fileFromPath (resDir),
		    Fs.fileFromPath (assetsDir));
		返回清单;
	}
}

Gradle配置

你可以使用下面的命令配置Gradle进行单元测试. 您可能需要根据您的项目需要修改所需的依赖项名称和版本.

/ / Robolectric
testCompile junit: junit: 4.12'
testCompile”组织.5: mockito-core: 1.9.5'
testCompile的com.squareup.匕首:匕首:1.2.2'
testProvided的com.squareup.匕首:dagger-compiler: 1.2.2'

testCompile的com.android.支持:support-v4:21.0.+'
testCompile的com.android.支持:appcompat-v7:21.0.3'

testCompile(“组织.robolectric: robolectric: 2.4') {
	排除模块:'classworlds'
	排除模块:'commons-logging'
	排除模块:'httpclient'
	排除模块:'maven-artifact'
	排除模块:'maven-artifact-manager'
	排除模块:'maven-error-diagnostics'
	排除模块:'maven-model'
	排除模块:'maven-project'
	排除模块:'maven-settings'
	排除模块:'plexus-container-default'
	排除模块:'plexus-interpolation'
	排除模块:'plexus-utils'
	排除模块:'wagon-file'
	排除模块:'wagon-http-lightweight'
	排除模块:'wagon-provider-api'
}

机器人电气和游戏服务

如果你正在使用 Google Play Services, 您必须为Play Services版本创建自己的整数常量,以便robolelectric在此应用程序配置中正常工作.


支持库的自动化依赖

Another interesting 测试问题是robolelectric不能正确地引用支持库. 解决方案是添加一个“项目”.“属性”文件保存到测试所在的模块. 例如,对于Support-v4和AppCompat库,文件应该包含:

android.library.reference.1=../../构建/中间体/ exploded-aar / com.android.支持/ support-v4/21.0.3
android.library.reference.2=../../构建/中间体/ exploded-aar / com.android.支持/ appcompat-v7/21.0.3

接受/回归测试

验收/回归测试自动化了在真实产品上测试的最后一步的一部分, 100% Android环境. 我们在这个级别上没有使用模拟的Android OS类——测试是在真实的设备和模拟器上运行的.

Android验收和回归测试

由于物理设备的多样性,这些情况使得该过程更加不稳定, 模拟器配置, 设备状态, 以及每个设备的功能集. Furthermore, 它高度依赖于操作系统的版本和手机的屏幕大小来决定内容的显示方式.

创建能够通过各种设备的正确测试有点复杂, 但一如既往,你应该有远大的梦想,但要从小事做起. 使用Robotium创建测试是一个迭代过程. 用一些小技巧,它可以简化很多.

Robotium

Robotium Android是开源的吗 测试自动化 框架自2010年1月以来一直存在. 值得一提的是,Robotium是付费解决方案,但附带公平的免费试用.

以加快编写Robotium测试的过程, 我们将从手工编写测试转向测试记录. 在代码质量和速度之间进行权衡. 如果您正在对用户界面进行重大更改, 您将从测试记录方法中获益良多,并且能够快速记录新的测试.

Testdroid记录器 是一个免费的测试记录器,创建Robotium测试,因为它记录点击你在用户界面上执行. 安装该工具非常容易,因为 在他们的文档中描述 附有一步一步的视频.

由于Testdroid记录器是一个Eclipse插件,我们在本文中引用的是Android工作室, 理想情况下,这将是一个令人担忧的理由. However, 在这种情况下,这不是问题, 因为您可以直接使用插件与APK并记录针对它的测试.

一旦您创建了测试, 你可以在Android工作室中复制粘贴它们, 以及Testdroid记录器所需的任何依赖项, 你准备好出发了. 记录的测试看起来像下面的类:

public class LoginTest extends ActivityInstrumentationTestCase2 {

private static final String LAUNCHER_ACTIVITY_CLASSNAME = "com.toptal.fitnesstracker.view.activity.SplashActivity”;
private static Class launchActivityClass;
static {
try {
	launchActivityClass = Class.forName (LAUNCHER_ACTIVITY_CLASSNAME);
		} catch (ClassNotFoundException e) {
			抛出新的RuntimeException(e);
		}
	}
	私人ExtSolo独奏;

	@SuppressWarnings(“unchecked”)
	公共LoginTest() {
		super((Class) launchActivityClass);
	}

	//在每个测试方法之前执行
	@Override
	public void setUp()抛出异常{
		super.setUp();
		solo = new ExtSolo(getInstrumentation(), getActivity(), this.getClass()
				.getCanonicalName (), getName ());
	}
	
	//在每个测试方法之后执行
	@Override
	public void tearDown()抛出异常{
		solo.finishOpenedActivities ();
		solo.tearDown();
		super.tearDown();
	}

	公共void testRecorded()抛出异常{
		try {
			assertTrue(
					"等待编辑文本(id: com).toptal.fitnesstracker.R.id.login_username_input)失败.",
					solo.waitForEditTextById (
							"com.toptal.fitnesstracker.R.id.login_username_input”,
							20000));
			solo.enterText(
					(EditText)独奏
							.findViewById(“com.toptal.fitnesstracker.R.id.login_username_input”),
					“user1@gmail.com");
			solo.sendKey (ExtSolo.ENTER);
			solo.sleep(500);
			assertTrue(
					"等待编辑文本(id: com).toptal.fitnesstracker.R.id.login_password_input)失败.",
					solo.waitForEditTextById (
							"com.toptal.fitnesstracker.R.id.login_password_input”,
							20000));
			solo.enterText(
					(EditText)独奏
							.findViewById(“com.toptal.fitnesstracker.R.id.login_password_input”),
					"123456");
			solo.sendKey (ExtSolo.ENTER);
			solo.sleep(500);
			assertTrue(
					等待按钮(id: com).toptal.fitnesstracker.R.id.parse_login_button)失败.",
					solo.waitForButtonById (
							"com.toptal.fitnesstracker.R.id.parse_login_button”,
							20000));
			solo.clickOnButton((按钮)独奏
                    .findViewById(“com.toptal.fitnesstracker.R.id.parse_login_button "));
            assertTrue("等待文本健身列表活动.",
                    solo.waitForActivity (FitnessActivity.class));
			assertTrue("等待文本KM.",
					solo.waitForText(“公里”,20000));

			/*
				允许正确单击ActionBar操作项的自定义类
			*/
            TestUtils.customClickOnView(独唱,R.id.action_logout);

            solo.waitForDialogToOpen ();
            solo.waitForText(“OK”);
            solo.clickOnText(“OK”);

            assertTrue("等待ParseLoginActivity退出后",独奏.waitForActivity (ParseLoginActivity.class));
            assertTrue(
                    等待按钮(id: com).toptal.fitnesstracker.R.id.parse_login_button)失败.",
                    solo.waitForButtonById (
                            "com.toptal.fitnesstracker.R.id.parse_login_button”,
                            20000));
		} catch (AssertionFailedError e) {
			solo.fail(
					"com.example.android.apis.test.Test.testRecorded_scr_fail”,
					e);
			throw e;
		} catch(异常e) {
			solo.fail(
					"com.example.android.apis.test.Test.testRecorded_scr_fail”,
					e);
			throw e;
		}
	}
}

如果仔细观察,您会注意到有多少代码是相当直接的.

在记录测试时,不要缺少“wait”语句. 等待对话框出现,活动出现,文本出现. 这将保证当您在当前屏幕上执行操作时,活动和视图层次结构已准备好与之交互. 同时,截屏. 自动化测试通常无人值守, 截图是你看到测试中实际发生的情况的一种方式.

无论测试通过还是失败,报告都是您最好的朋友. 你可以在构建目录“module/build/outputs/reports”下找到它们:

测试报告

理论上, QA team 能否记录测试并对其进行优化. 通过在标准化模型中投入精力来优化测试用例,这是可以做到的. 当你正常记录测试时, 你总是需要调整一些东西来让它完美地工作.

Finally, 在Android工作室中运行这些测试, 您可以选择它们并像运行单元测试一样运行它们. 从终端,它是一行代码:

gradle connectedAndroidTest

测试性能

使用robolelectric进行Android单元测试非常快, 因为它直接在机器上的JVM中运行. 相比之下,在模拟器和物理设备上进行验收测试要慢得多. 取决于您正在测试的流的大小, 每个测试用例可能需要几秒钟到几分钟的时间. 验收测试阶段应该作为持续集成服务器上自动构建过程的一部分使用.

在多个设备上并行化可以提高速度. 看看这个伟大的工具 杰克沃顿商学院 还有Square的人 http://square.github.io/spoon/. 它也有一些不错的报道.

的外卖

有各种各样的Android测试工具可用, 随着生态系统的成熟, 设置可测试环境和编写测试的过程将变得更加容易. 还有更多的挑战需要解决, 并且有一个广泛的开发人员社区来解决日常问题, 这里有很多建设性讨论和快速反馈的空间.

使用本Android测试教程中描述的方法来指导您解决前面的挑战. 当你遇到问题的时候, 查看本文或其中链接的参考资料,了解已知问题的解决方案.

在以后的文章中, 我们将讨论并行化, 构建自动化, 持续集成, Github / BitBucket都钩, 工件的版本, 以及更深入地管理大型移动应用程序项目的最佳实践.

就这一主题咨询作者或专家.
预约电话
马尔科·维塔斯的头像
Marko Vitas

Located in 克罗地亚的萨格勒布

成员自 2015年2月24日

作者简介

拥有计算机科学学位的高级Android开发人员, Marko拥有6年以上的应用开发经验, 包括一个安全的手机银行应用程序.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

以前在

Infinum

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

加入总冠军® community.