您好,欢迎来到三六零分类信息网!老站,搜索引擎当天收录,欢迎发信息

Android单元测试与模拟测试详解

2024/5/17 22:35:00发布34次查看
测试与基本规范
为什么需要测试?
为了稳定性,能够明确的了解是否正确的完成开发。
更加易于维护,能够在修改代码后保证功能不被破坏。
集成一些工具,规范开发规范,使得代码更加稳定( 如通过 phabricator differential 发diff时提交需要执行的单元测试,在开发流程上就可以保证远端代码的稳定性)。
2. 测什么?
一般单元测试:
列出想要测试覆盖的异常情况,进行验证。
性能测试。
模拟测试: 根据需求,测试用户真正在使用过程中,界面的反馈与显示以及一些依赖系统架构的组件的应用测试。
3. 需要注意
考虑可读性,对于方法名使用表达能力强的方法名,对于测试范式可以考虑使用一种规范, 如 rspec-style。方法名可以采用一种格式,如: [测试的方法]_[测试的条件]_[符合预期的结果]。
不要使用逻辑流关键字(if/else、for、do/while、switch/case),在一个测试方法中,如果需要有这些,拆分到单独的每个测试方法里。
测试真正需要测试的内容,需要覆盖的情况,一般情况只考虑验证输出(如某操作后,显示什么,值是什么)。
考虑耗时,android studio默认会输出耗时。
不需要考虑测试private的方法,将private方法当做黑盒内部组件,测试对其引用的public方法即可;不考虑测试琐碎的代码,如getter或者setter。
每个单元测试方法,应没有先后顺序;尽可能的解耦对于不同的测试方法,不应该存在test a与test b存在时序性的情况。
4. 创建测试
选择对应的类
将光标停留在类名上
按下alt + enter
在弹出的弹窗中选择create test
android studio中的单元测试与模拟测试
control + shift + r (android studio 默认执行单元测试快捷键)。
1. 本地单元测试
直接在开发机上面进行运行测试。
在没有依赖或者仅仅只需要简单的android库依赖的情况下,有限考虑使用该类单元测试。
./gradlew check
(1)代码存储 
如果是对应不同的flavor或者是build type,直接在test后面加上对应后缀(如对应名为myflavor的单元测试代码,应该放在src/testmyflavor/java下面)。
src/test/java
(2)google官方推荐引用
dependencies { // required -- junit 4 framework,用于单元测试,google官方推荐 testcompile 'junit:junit:4.12' // optional -- mockito framework,用于模拟架构,google官方推荐 // http://www.manongjc.com/article/1546.html testcompile 'org.mockito:mockito-core:1.10.19' }
(3)junit
annotation
2. 模拟测试
需要运行在android设备或者虚拟机上的测试。
主要用于测试: 单元(android sdk层引用关系的相关的单元测试)、ui、应用组件集成测试(service、content provider等)。
./gradlew connectedandroidtest
(1)代码存储:
src/androidtest/java
(2)google官方推荐引用
dependencies { androidtestcompile 'com.android.support:support-annotations:23.0.1' androidtestcompile 'com.android.support.test:runner:0.4.1' androidtestcompile 'com.android.support.test:rules:0.4.1' // optional -- hamcrest library androidtestcompile 'org.hamcrest:hamcrest-library:1.3' // optional -- ui testing with espresso // http://www.manongjc.com/article/1546.html androidtestcompile 'com.android.support.test.espresso:espresso-core:2.2.1' // optional -- ui testing with ui automator androidtestcompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1' }
(3)常见的ui测试
需要模拟android系统环境。
主要三点:
ui加载好后展示的信息是否正确。
在用户某个操作后ui信息是否展示正确。
展示正确的页面供用户操作。
(4)espresso
谷歌官方提供用于ui交互测试
import static android.support.test.espresso.espresso.onview; import static android.support.test.espresso.action.viewactions.click; import static android.support.test.espresso.assertion.viewassertions.matches; import static android.support.test.espresso.matcher.viewmatchers.isdisplayed; import static android.support.test.espresso.matcher.viewmatchers.withid; // 对于id为r.id.my_view的view: 触发点击,检测是否显示 onview(withid(r.id.my_view)).perform(click()) .check(matches(isdisplayed())); // 对于文本打头是abc的view: 检测是否没有enable onview(withtext(startswith(abc))).check(matches(not(isenabled())); // 按返回键 pressback(); // 对于id为r.id.button的view: 检测内容是否是start new activity // http://www.manongjc.com/article/1537.html onview(withid(r.id.button)).check(matches(withtext((start new activity)))); // 对于id为r.id.viewid的view: 检测内容是否不包含yyzz onview(withid(r.id.viewid)).check(matches(withtext(not(containsstring(yyzz))))); // 对于id为r.id.inputfield的view: 输入newtext,然后关闭软键盘 onview(withid(r.id.inputfield)).perform(typetext(newtext), closesoftkeyboard()); // 对于id为r.id.inputfield的view: 清除内容 onview(withid(r.id.inputfield)).perform(cleartext());
启动一个打开activity的intent
@runwith(androidjunit4.class) public class secondactivitytest { @rule public activitytestrule rule = new activitytestrule(secondactivity.class, true, // 这个参数为false,不让secondactivity自动启动 // 如果为true,将会在所有@before之前启动,在最后一个@after之后关闭 false); @test public void demonstrateintentprep() { intent intent = new intent(); intent.putextra(extra, test); // 启动secondactivity并传入intent rule.launchactivity(intent); // 对于id为r.id.display的view: 检测内容是否是text // http://www.manongjc.com/article/1532.html onview(withid(r.id.display)).check(matches(withtext(test))); } }
(5)异步交互
建议关闭设备中”设置->开发者选项中”的动画,因为这些动画可能会是的espresso在检测异步任务的时候产生混淆: 窗口动画缩放(window animation scale)、过渡动画缩放(transition animation scale)、动画程序时长缩放(animator duration scale)。
针对asynctask,在测试的时候,如触发点击事件以后抛了一个asynctask任务,在测试的时候直接onview(withid(r.id.update)).perform(click()),然后直接进行检测,此时的检测就是在asynctask#onpostexecute之后。
// 通过实现idlingresource,block住当非空闲的时候,当空闲时进行检测,非空闲的这段时间处理异步事情 public class intentserviceidlingresource implements idlingresource { resourcecallback resourcecallback; private context context; public intentserviceidlingresource(context context) { this.context = context; } @override public string getname() { return intentserviceidlingresource.class.getname(); } @override public void registeridletransitioncallback( resourcecallback resourcecallback) { this.resourcecallback = resourcecallback; } @override public boolean isidlenow() { // 是否是空闲 // 如果intentservice 没有在运行,就说明异步任务结束,intentservice特质就是启动以后处理完intent中的事务,理解关闭自己 // http://www.manongjc.com/article/1531.html boolean idle = !isintentservicerunning(); if (idle && resourcecallback != null) { // 回调告知异步任务结束 resourcecallback.ontransitiontoidle(); } return idle; } private boolean isintentservicerunning() { activitymanager manager = (activitymanager) context.getsystemservice(context.activity_service); // get all running services list runningservices = manager.getrunningservices(integer.max_value); // check if our is running for (activitymanager.runningserviceinfo info : runningservices) { if (myintentservice.class.getname().equals(info.service.getclassname())) { return true; } } return false; } } // 使用intentserviceidlingresource来测试,myintentservice服务启动结束这个异步事务,之后的结果。 @runwith(androidjunit4.class) public class integrationtest { @rule public activitytestrule rule = new activitytestrule(mainactivity.class); intentserviceidlingresource idlingresource; @before public void before() { instrumentation instrumentation = instrumentationregistry.getinstrumentation(); context ctx = instrumentation.gettargetcontext(); idlingresource = new intentserviceidlingresource(ctx); // 注册这个异步监听 espresso.registeridlingresources(idlingresource); } @after public void after() { // 取消注册这个异步监听 espresso.unregisteridlingresources(idlingresource); } @test public void runsequence() { // mainactivity中点击r.id.action_settings这个view的时候,会启动myintentservice onview(withid(r.id.action_settings)).perform(click()); // 这时候intentserviceidlingresource#isidlenow会返回false,因为myintentservice服务启动了 // 这个情况下,这里会block住............. // 直到intentserviceidlingresource#isidlenow返回true,并且回调了intentserviceidlingresource#ontransitiontoidle // 这个情况下,继续执行,这时我们就可以测试异步结束以后的情况了。 onview(withtext(broadcast)).check(matches(notnullvalue())); } }
(6)自定义匹配器
// 定义 public static matcher withitemhint(string itemhinttext) { checkargument(!(itemhinttext.equals(null))); return withitemhint(is(itemhinttext)); } public static matcher withitemhint(final matcher matchertext) { checknotnull(matchertext); return new boundedmatcher(edittext.class) { @override public void describeto(description description) { description.appendtext(with item hint: + matchertext); } @override protected boolean matchessafely(edittext edittextfield) { // 取出hint,然后比对下是否相同 // http://www.manongjc.com/article/1524.html return matchertext.matches(edittextfield.gethint().tostring()); } }; } // 使用 onview(withitemhint(test)).check(matches(isdisplayed()));
拓展工具
1. assertj android
square/assertj-android
极大的提高可读性。
import static org.assertj.core.api.assertions.*; // 断言: view是gone的 assertthat(view).isgone(); myclass test = new myclass(frodo); myclass test1 = new myclass(sauron); myclass test2 = new myclass(jacks); list testlist = new arraylist(); testlist.add(test); testlist.add(test1); // 断言: test.getname()等于frodo assertthat(test.getname()).isequalto(frodo); // 断言: test不等于test1并且在testlist中 // http://www.manongjc.com/article/1519.html assertthat(test).isnotequalto(test1) .isin(testlist); // 断言: test.getname()的字符串,是由fro打头,以do结尾,忽略大小写会等于frodo assertthat(test.getname()).startswith(fro) .endswith(do) .isequaltoignoringcase(frodo); // 断言: testlist有2个数据,包含test,test1,不包含test2 assertthat(list).hassize(2) .contains(test, test1) .doesnotcontain(test2); // 断言: 提取testlist队列中所有数据中的成员变量名为name的变量,并且包含name为frodo与sauron // 并且不包含name为jacks assertthat(testlist).extracting(name) .contains(frodo, sauron) .doesnotcontain(jacks);
2. hamcrest
javahamcrest
通过已有的通配方法,快速的对代码条件进行测试
org.hamcrest:hamcrest-junit:(version)
import static org.hamcrest.matcherassert.assertthat; import static org.hamcrest.matchers.is; import static org.hamcrest.matchers.equalto; // 断言: a等于b assertthat(a, equalto(b)); assertthat(a, is(equalto(b))); assertthat(a, is(b)); // 断言: a不等于b assertthat(actual, is(not(equalto(b)))); list list = arrays.aslist(5, 2, 4); // 断言: list有3个数据 assertthat(list, hassize(3)); // 断言: list中有5,2,4,并且顺序也一致 assertthat(list, contains(5, 2, 4)); // 断言: list中包含5,2,4 assertthat(list, containsinanyorder(2, 4, 5)); // 断言: list中的每一个数据都大于1 // http://www.manongjc.com/article/1507.html assertthat(list, everyitem(greaterthan(1))); // 断言: fellowship中包含有成员变量race,并且其值不是orc assertthat(fellowship, everyitem(hasproperty(race, is(not((orc)))))); // 断言: object1中与object2相同的成员变量都是相同的值 assertthat(object1, samepropertyvaluesas(object2)); integer[] ints = new integer[] { 7, 5, 12, 16 }; // 断言: 数组中包含7,5,12,16 assertthat(ints, arraycontaining(7, 5, 12, 16));
(1)几个主要的匹配器:
(2)自定义匹配器
// 自定义 import org.hamcrest.description; import org.hamcrest.typesafematcher; public class regexmatcher extends typesafematcher { private final string regex; public regexmatcher(final string regex) { this.regex = regex; } @override public void describeto(final description description) { description.appendtext(matches regular expression=` + regex + `); } @override public boolean matchessafely(final string string) { return string.matches(regex); } // 上层调用的入口 public static regexmatcher matchesregex(final string regex) { return new regexmatcher(regex); } } // 使用 string s = aaabbbaaa; assertthat(s, regexmatcher.matchesregex(a*b*a));
3. mockito
mockito
mock对象,控制其返回值,监控其方法的调用。
org.mockito:mockito-all:(version)
// import如相关类 import static org.mockito.mockito.mock; import static org.mockito.mockito.verify; // 创建一个mock的对象 myclass test = mock(myclass.class); // 当调用test.getuniqueid()的时候返回43 when(test.getuniqueid()).thenreturn(43); // 当调用test.compareto()传入任意的int值都返回43 when(test.compareto(anyint())).thenreturn(43); // 当调用test.compareto()传入的是target.class类型对象时返回43 when(test.compareto(isa(target.class))).thenreturn(43); // 当调用test.close()的时候,抛ioexception异常 dothrow(new ioexception()).when(test).close(); // 当调用test.execute()的时候,什么都不做 donothing().when(test).execute(); // 验证是否调用了两次test.getuniqueid() // http://www.manongjc.com/article/1503.html verify(test, times(2)).getuniqueid(); // 验证是否没有调用过test.getuniqueid() verify(test, never()).getuniqueid(); // 验证是否至少调用过两次test.getuniqueid() verify(test, atleast(2)).getuniqueid(); // 验证是否最多调用过三次test.getuniqueid() verify(test, atmost(3)).getuniqueid(); // 验证是否这样调用过:test.query(test string) verify(test).query(test string); // 通过mockito.spy() 封装list对象并返回将其mock的spy对象 list list = new linkedlist(); list spy = spy(list); // 指定spy.get(0)返回foo doreturn(foo).when(spy).get(0); assertequals(foo, spy.get(0));
对访问方法时,传入参数进行快照
import org.mockito.argumentcaptor; import org.mockito.captor; import static org.junit.assert.assertequals; @captor private argumentcaptor captor; @test public void testcapture(){ myclass test = mock(myclass.class); test.compareto(3, 4); verify(test).compareto(captor.capture(), eq(4)); assertequals(3, (int)captor.getvalue()); // 需要特别注意,如果是可变数组(vargars)参数,如方法 test.dosomething(string... params) // 此时是使用argumentcaptor,而非argumentcaptor argumentcaptor varargs = argumentcaptor.forclass(string.class); test.dosomething(param-1, param-2); verify(test).dosomething(varargs.capture()); // 这里直接使用getallvalues()而非getvalue(),来获取可变数组参数的所有传入参数 assertthat(varargs.getallvalues()).contains(param-1, param-2); }
(1)对于静态的方法的mock:
可以使用 powermock:
org.powermock:powermock-api-mockito:(version) & org.powermock:powermock-module-junit4:(version)(for powermockrunner.class)
@runwith(powermockrunner.class) @preparefortest({staticclass1.class, staticclass2.class}) public class mytest { @test public void testsomething() { // mock完静态类以后,默认所有的方法都不做任何事情 mockstatic(staticclass1.class); when(staticclass1.getstaticmethod()).andreturn(anything); // 验证是否staticclass1.getstaticmethod()这个方法被调用了一次 verifystatic(time(1)); staticclass1.getstaticmethod(); when(staticclass1.getstaticmethod()).andreturn(what ever); // 验证是否staticclass2.getstaticmethod()这个方法被至少调用了一次 verifystatic(atleastonce()); staticclass2.getstaticmethod(); // 通过任何参数创建file的实力,都直接返回fileinstance对象 whennew(file.class).withanyarguments().thenreturn(fileinstance); } }
或者是封装为非静态,然后用mockito:
class foowraper{ void somemethod() { foo.somestaticmethod(); } }
4. robolectric
robolectric
让模拟测试直接在开发机上完成,而不需要在android系统上。所有需要使用到系统架构库的,如(handler、handlerthread)都需要使用robolectric,或者进行模拟测试。
主要是解决模拟测试中耗时的缺陷,模拟测试需要安装以及跑在android系统上,也就是需要在android虚拟机或者设备上面,所以十分的耗时。基本上每次来来回回都需要几分钟时间。针对这类问题,业界其实已经有了一个现成的解决方案: pivotal实验室推出的robolectric。通过使用robolectrict模拟android系统核心库的shadow classes的方式,我们可以像写本地测试一样写这类测试,并且直接运行在工作环境的jvm上,十分方便。
5. robotium
robotiumtech/robotium
(integration tests)模拟用户操作,事件流测试。
@runwith(robolectrictestrunner.class)
@config(constants = buildconfig.class)
public class myactivitytest{
@test public void dosomethingtests(){ // 获取application对象 application application = runtimeenvironment.application; // 启动welcomeactivity welcomeactivity activity = robolectric.setupactivity(welcomeactivity.class); // 触发activity中id为r.id.login的view的click事件 // http://www.manongjc.com/article/1502.html activity.findviewbyid(r.id.login).performclick(); intent expectedintent = new intent(activity, loginactivity.class); // 在activity之后,启动的activity是否是loginactivity assertthat(shadowof(activity).getnextstartedactivity()).isequalto(expectedintent); } }
通过模拟用户的操作的行为事件流进行测试,这类测试无法避免需要在虚拟机或者设备上面运行的。是一些用户操作流程与视觉显示强相关的很好的选择。
6. test butler
linkedin/test-butler
避免设备/模拟器系统或者环境的错误,导致测试的失败。
通常我们在进行ui测试的时候,会遇到由于模拟器或者设备的错误,如系统的crash、anr、或是未预期的wifi、cpu罢工,或者是锁屏,这些外再环境因素导致测试不过。test-butler引入就是避免这些环境因素导致ui测试不过。
该库被谷歌官方推荐过,并且收到谷歌工程师的review。
拓展思路
1. android robots
instrumentation testing robots – jake wharton
假如我们需要测试: 发送 $42 到 “foo@bar.com”,然后验证是否成功。
(1)通常的做法
(2)robot思想
在写真正的ui测试的时候,只需要关注要测试什么,而不需要关注需要怎么测试,换句话说就是让测试逻辑与view或presenter解耦,而与数据产生关系。
首先通过封装一个robot去处理how的部分:
然后在写测试的时候,只关注需要测试什么:
终的思想原理
该用户其它信息

VIP推荐

免费发布信息,免费发布B2B信息网站平台 - 三六零分类信息网 沪ICP备09012988号-2
企业名录 Product