• <code id="58gs9"></code>

      <mark id="58gs9"></mark>
      1. 您好£¬欢迎来到源码搜藏网£¡分享精神£¬快乐你我£¡
        [加入VIP] 设为首页 | 收藏本站 | 网站地图 | Sitemap | TAG标签
      2. 首 页
      3. 在线工具
      4. jquery手册
      5. 当前位置£º首页 > 安卓源码 > 技术博客 >

        Android开发中的自动化测试

        时间£º2019-02-15 14:13 来源£º互联网 作者£º源码搜藏 浏览£º收藏 挑错 推荐 打印

        Instrumentation介绍 Instrumentation是个什么东西£¿ Instrumentation测试 Instrumentation原理介绍 一¡¢Instrumentation是个什么东西£¿ Instrumentation是位于android.app包下£¬与Activity处于同一级目录£¬它是Android系统中一系列控制方法的集合£¨俗称hook

        Instrumentation介绍

        • Instrumentation是个什么东西£¿

        • Instrumentation测试

        • Instrumentation原理介绍

        一¡¢Instrumentation是个什么东西£¿

        • Instrumentation是位于android.app包下£¬与Activity处于同一级目录£¬它是Android系统中一系列控制方法的集合£¨俗称hook£©¡£这些hook可以在正常的生命周期之外控制Android控件的运行£¬?#37096;?#25511;制Android如何加载应用程序¡£

        • 可以说Instrumentation就是AndroidSDK在Junit上的扩展£¬提供了AndroidTestCase类及其系列子类£¬其中最重要的一个类是ActivityInstrumentationTestCase2¡£

        • Instrumentation将在任何应用程序启动之前初始化£¬通过它来检测系统与应用程序之间的所有的?#25442;¥¡?/p>

        • 通过Instrumentation启动时£¬测试程序与被测应用运行在同一进程的不同线程中¡£

        Android开发中的自动化测试

        • application package 与 test package 处于同一个进程¡£

        • Android测试框架基于JUnit£¬因此可以直接使用JUnit来测试一些与Android平台不相关的类£¬或者使用Android的JUnit扩展来测试Android组件£»

        • Android JUnit扩展提供了对Android特定组件£¨Activity£¬Service等£©的测试支持£¬也就是对mock的支持£»

        二¡¢如何使用Instrumentation进行单元测试£¿

        • 环境配置£º

        在AndroidMainfast.xml文件中加入一下代码£º

        <instrumentation  
            android:name="android.test.InstrumentationTestRunner"
        
            android:targetPackage="com.xxx.xxx" />

        注意£ºtargetPackage是被测应用的包名

        或者在App module gradle中增加以下下配置£º

        defaultConfig {
            testInstrumentationRunner "android.support.test.InstrumentationTestRunner"
        }
        • 使用Instrumentation进行自动化测试步骤£º

        1¡¢ 启动应用

        Intent intent = new Intent();
        intent.setClassName(“packageName”,”className”);
        intent,setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        MainActivity activity = (MainActivity)getInstrumentation().startActivitySync(intent);

        2¡¢编辑界面£º

        Button button = activity.findViewById(R.id.btn);

        3¡¢结果提交£º

        button.performClick();

        4¡¢界面跳转£º

        ActivityMonitor monitor = getInstrumentation().addMonitor(String cls, ActivityResult result, boolean block);
        ChangeActivity ca = (ChangeActivity)getInstrumentation().waitForMonitor(monitor);

        5¡¢验证结果£º

        assertTrue(ca != null);

        三¡¢Instrumentation源码分析£¿

        1£©¡¢测试框架如何运行£¿

        想要了解测试框架运行过程需从InstrumentationTestRunner类入手£¬此类为Android测试框架启动入口¡£
        整个运行流程如下£º

        • 创建线程?#21644;?#36807;InstrumentationTestRunner创建Instrumentation专属线程InstrumentationThread¡£

        • 获取线程?#21644;?#36807;InstrumentationTestRunner获取线程

        • 执行用例?#21644;?#36807;AndroidTestRunner对象循?#20998;?#34892;测试用例

        通过Instrumentation框架可以直接操作activity的生命周期函数¡£
        测试activity的基本测试类为InstrumentationTestCase£¬它提供了Instrumentation接口给TestCase的子类£¬为了支持activity的测试£¬InstrumentationtTestCase提供了下面功能£º

        • 生命周期的控制£º使用Instrumentation可以启动£¬暂停£¬终止被测试的activity¡£

        • Dependency Injection£¨?#35272;?#27880;入£©£ºInstrumentation允许创建一些mock对象£¬如Context£¬Application来帮助测试activity£¬从而帮助你控制测试环境并和?#23548;视?#29992;的其他部分隔离开来¡£

        • 用户界面?#25442;¥£?#20320;可以使用Instrumentation向UI发送按键触摸?#24405;þ¡?/p>

        以下是由TestCase派生而来的测试类£º

        • ActivityInstrumationTestCase2 £º 通常用于多个activity的功能测试£¬它使用正常的系统框架来运行activity£¨使用应用程序本身£©£¬并使用正常系统Context£¨非Mock£©来测试activity的功能£¬允许你创建一些mock Intent用来测试activity的响应£¬这种case不允许使用mock的Context和Application对象测试£¬也就是?#30340;?#24517;须使用和应用程序?#23548;试?#34892;的环境测试¡£

        • ActivityUnitTestCase ?#21644;?#24120;用来测试单独的activity£¬在启动被测试的activity之前£¬你可以Inject一个假的Context或是Application£¬使用这个mock的Context中一个隔离环境中运行被测试activity¡£通常用于activity的单元测试£¬而不和Android系统进行?#25442;¥¡?/p>

        • SingleLaunchActivityTestCase£º用于测试单个activity£¬和ActivityUnitTestCase不同的是£¬它只运行setUp和tearDown一次£¬而不是在运行testCase中每个test Method前后运行setUp和tearDown£¬它可以保证运行多个测试之间fixture?#25442;?#34987;重置£¬从而可以用来测试一些有关联的方法¡£

        说明£º以上为Instrumentation单元测试框架的内容£¬通过配置InstrumentationTestRunner启动器来执行自动化测试脚本£¬But在最新SDK中与InstrumentationTestRunner相关的所有类都被废弃了£¬Google推荐使用最新的测试框架来编写测试代码¡££¨AndroidJUnitRunner + Espresso + UIAutomater£©

        AndroidJUnitRunner

        AndroidJUnitRunner是一个可以用来运行JUnit 3和JUnit 4样式的测试类的Test Runner£¬并且同时支持Espresso和UI Automator¡£这是对于之前的InstrumentationTestRunner的一个升级£¬如果你去查看Gradle文档中对于Testing配置的说明£¬会发现推荐的Test Runner为AndroidJUnitRunner¡£InstrumentationTestRunner只支持JUnit 3样式的测试用例£¬而我们在写Android测试用例时应该尽可能使用JUnit 4样式来实现¡£

        相对于Junit 3£¬JUnit 4有如下改进£º

        • 测试类不需要再?#22363;Ðjunit.framework.TestCase类£»

        • 测试方法名不再需要以test开头£»

        • 可以使用类似@Test,@Before,@After等注解来管理自己的测试方法£»

        • 增加了一些Assert方法£»

        • 支持对assert方法的static导入¡£

        下面来看一个例子¡£如下的代码段采用了JUnit 4风格进行编写£¬并且调用了Espresso的API来进行了一些测试£º

        @RunWith(AndroidJUnit4.class)
        @LargeTest
        public class MainActivityInstrumentationTest {
        
            @Rule
            public ActivityTestRule mActivityRule = new ActivityTestRule<>(
            MainActivity.class);
        
            @Test
            public void sayHello(){
                onView(withText("Say hello!")).perform(click());
                onView(withId(R.id.textView)).check(matches(withText("Hello, World!")));
            }
        }

        从以上代码可以看到£¬JUnit 4支持使用如下注解来管理整个测试用例£º

        • @Before: 标?#23545;?#36816;行测试方法之前运行的代码¡£可以支持同一个Class中有多个@Before£¬但是这些方法的执行顺序是随机的¡£该注解替代了JUnit 3中的setUp()方法¡£

        • @After: 标?#23545;?#36816;行测试方法结束之后运行的代码¡£可以在其中做一些释放?#35797;?#30340;操作¡£该注解替代了JUnit 3中的tearDown()方法¡£

        • @Test: 标识一个测试方法¡£一个测试类中可以有多个测试方法£¬每个测试方法需要用一个@Test注解来标识¡£

        • @Rule: 简单来说£¬是为各个测试方法提供一些支持¡£具体来说£¬比如我需要测试一个Activity£¬那么我可以在@Rule注解下面采用一个ActivityTestRule£¬该类提供了对相应Activity的功能测试的支持¡£该类可以在@Before和@Test标识的方法执行之前确保将Activity运行起来£¬并且在所有@Test和@After方法执行结束之后将Activity杀死¡£在整个测试期间£¬每个测试方法都可以直接对相应Activity进行修改和?#26790;Ê¡?/p>

        • @BeforeClass: 为测试类标识一个static方法£¬在测试之前只执行一次¡£

        • @AfterClass: 为测试类标识一个static方法£¬在所有测试方法结束之后只执行一次¡£

        • @Test(timeout=<milliseconds>): 为测试方法设定超时时间¡£

        Espresso详解

        Espresso是一个新工具£¬相对于其他工具£¬API更加精确¡£并且规模更小¡¢更简洁并且容易学习¡£它最初是2013年GTAC大会上推出的£¬目标是让开发者写出更简洁的针对APP的UI测试代码¡£

        优点£º

        • 代码快速上手

        • 容易扩展

        • 无需考虑复杂的多线程

        • 有Google做?#21487;?/p>

        缺点£º

        • 不支持跨应用的UI测试

        Espresso 的主要组件£º

        onView(ViewMatchers).perform(ViewActions).check(ViewAssertions)

        • Espresso – 与视图£¨views£©?#25442;?#30340;入口£¬并暴露了一些视图£¨views£©无关的API£¨例如回退按钮£©¡£

        • ViewMatchers – 实?#21046;?#37197;器的一组对象¡£允许可以通过多次的onView方法£¬在层次图中找到目标视图£¨views£©¡£

        • ViewActions – 对视图触发动作£¨例如点击£©¡£

        • ViewAssertions – 用于插入测试关键点的一组断言£¬可用于判断某视图£¨view£©的状态¡£

        可以看出£¬与其他框架相比£¬Espresso代码集成度更高£¬功能分块更加集中£ºonView用于定位视图£¬perform用于产生?#24405;þ£¬check用于检测checkpoint¡£

        Espresso环境搭建

        • 在Android Studio中新建一个Project;

        • 修改Project中App/build.gradle脚本(Android studio2.2默认集成了Espresso) 

        主要修改3处£º

        • 在defaultConfig内增加

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        • 增加packagingOptions,避免编译时候Liscens的冲突;

        • 在dependencies中增加Espresso相关的引用;

        defaultConfig {
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
        packagingOptions {
            exclude 'LICENSE.txt' }
        }
        
        dependencies {
            testCompile 'junit:junit:4.12'
            androidTestCompile ('com.android.support.test:runner:0.5'){
                exclude group: 'com.android.support',module: 'support-annotations'
            }
            androidTestCompile ('com.android.support.test:rules:0.5') {
                exclude group: 'com.android.support',module: 'support-annotations'
            }
            androidTestCompile ('com.android.support.test.espresso:espresso-core:2.2.2'){
                exclude group: 'com.android.support',module: 'support-annotations'
            }
            androidTestCompile ('com.android.support.test.espresso:espresso-idling-resource:2.2.2'){
                exclude group: 'com.android.support',module: 'support-annotations'
            }
            androidTestCompile ('com.android.support.test.espresso:espresso-intents:2.2.2'){
                exclude group: 'com.android.support',module: 'support-annotations'
            }
            androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.2.2') {
                exclude group: 'com.android.support',module: 'support-annotations'
                exclude group: 'com.android.support', module: 'appcompat'
                exclude group: 'com.android.support', module: 'appcompat-v7'
                exclude group: 'com.android.support', module: 'design'
                exclude group: 'com.android.support', module: 'support-v4'
                exclude module: 'recyclerview-v7'
            }
        }

        说明£º

        espresso-core£ºespresso的基础库

        espresso-idling-resource£º异步任务相关的库

        espresso-intents£º提供对intent的支持库

        espresso-contrib£º提供对特定组件£¨如£ºrecycleView等£©的支持库

        espresso-web:提供对webView测试的支持库

        Espresso API 鼓励测试者以用户会怎样与应用?#25442;?#30340;方式进行思考来定位 UI 元素并与它们?#25442;¥¡?#21516;时£¬框架不允许直接使用应用的活动和视图£¬因为在非 UI 线程持有此类对象并对它们操作是造成测试花屏的主要原因¡£因此£¬你?#25442;?#22312; Espresso API 中看到诸如 getView 或 getCurrentActivity 等方法¡£但你仍然可以通过实现 ViewAction 和 ViewAssertion 来对视图进行安全操作¡£

        例如£º

        onView(withText("test")).check(matches(isDisplayed())).perform(click());

        查找?#20801;?#25991;本为Test所对应的View?#20801;?#22312;界面上£¬并点击该View

        使用 onView 查找视图

        多数情况下£¬onView 方法使用 hamcrest 匹配器以期望在当前视图结构里匹配一个£¨唯一的£©视图¡£该匹配器十分?#30475;?#32780;且对用过 Mockito 或 JUnit 的人而言并不陌生¡£
        想要查找的视图一般会?#24418;?#19968;的 ?R.id? 值£¬使用简单的 ?withId? 匹配器可以缩小搜索?#27573;§¡£È欢ø£?#24403;你在测试开发阶段£¬无法确定 ?R.id值是合理的?¡£例如£¬指定的视图可能没有 R.id? 值或该值不唯一¡£这将使一般的 instrumentation 测试变?#20040;?#24369;而复杂£¬因为通用的获取视图方式£¨通过 findViewById()£©已经不适用了¡£因此£¬你可能需要获取持有视图的私有对象 Activity 或 Fragment£¬或者找到一个已知其 ?R.id? 值的父容器£¬然后在其中定位到特定的视图¡£

        Espresso 处理?#26790;?#39064;的方式很干脆£¬它允许你使用已存在的或自定义的 ViewMatcher 来限定视图查找¡£
        通过 ?R.id? 查找视图£º

        onView(withId(R.id.my_view))

        有时£¬?R.id?值会?#27426;?#20010;视图共享,此时你需要找到一个能唯一确定的属性£¬你可以通过使用组合匹配器结合该属性来缩小搜索?#27573;§£?/p>

        onView(allOf(withId(R.id.my_view), withText("Hello!")))

        你?#37096;?#20197;使用 ?not? 反转匹配£º

        onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

        你可以在 ViewMatchers 类中查看 Espresso 提供的视图匹配器¡£

        注意£º在一个?#32487;?#30340;应用中£¬所有用户可与之?#25442;?#30340;视?#32423;加?#35813;包含说明文字或有一个内容描述¡£如果你不能通过使用 ‘withText’ 或 ‘withContentDescripiton’ 来缩小 onView 的搜索?#27573;§£?#21487;以认为这是一个很明显的 bug¡£
        如果目标视图在一个 ?AdapterView?£¨如 ?ListView?£¬?GridView?£¬?Spinner?£©中£¬将不能使用 onView? 方法£¬推荐使用 ?onData? 方法¡£

        在视图上执行操作

        当为目标视图找到了合适的适配器后£¬你将可以通过 ?perform? 方法在该视图上执行 ?ViewAction?¡£
        例如£¬点击该视图£º

        onView(…).perform(click());

        如果操作的视图在 ?ScrollView?£¨水平或垂直方向£©中£¬需要考虑在对该视图执行操作£¨如 ?click()? 或 ?typeText()?£©之前通过 ?scrollTo()? 方法使其处于?#20801;?#29366;态¡£这样就保证了视图在执行其他操作之前是?#20801;?#30528;的¡£

        onView(…).perform(scrollTo(), click());

        注意£º如果视图已经是?#20801;?#29366;态£¬ ?scrollTo()? 将?#25442;?#23545;界面有影响¡£因此£¬当视图的可见性取决于屏幕的大小时£¨例如£¬同时在大屏和小屏上执行测试时£©£¬你可以安全的使用该方法¡£
        你可以在 ViewActions 类中产看 Espresso 提供的视图操作¡£

        检查一个视图是否满足断言

        断言可以通过 ?check()? 方法应用在当前选中的视图上¡£最常用的是 ?matches()? 断言£¬它使用一个 ?ViewMatcher? 来判?#31995;?#21069;选中视图的状态¡£
        例如£¬检查一个视?#21152;?#26377; “Hello!”文本£º

        onView(…).check(matches(withText("Hello!")));

        注意£º不要将 “assertions” 作为 onView 的参数传入£¬而要在检查代码块中明?#20998;?#23450;你检查的内容£¬
        例如£º如果你想要断言视图的内容是 “Hello!” £¬以下做法是错误的£º

        // Don't use assertions like withText inside onView.
        onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));
        //use it
        onView(withId(...)).check(matches(withText("Hello!")));

        从另一个角度讲£¬如果你想要断言一个包含 “Hello!” 文本的视图是可见的£¨例如£¬在修改了该视图的可见性标志之后£©£¬这段代码是正确的¡£

        注意£º请留意断言一个视图没有?#20801;?#21644;断言一个视图不在当前视图结构之间的区别¡£

        在 ?AdapterView? 控制器£¨ListView, GridView, ...£©中使用 onData

        AdapterView? 是一个从适配器中动态加载数据的特殊控件¡£最常见的 ?AdapterView? 是 ListView?¡£与像 ?LinearLayout? 这样的静态控件相反£¬在当前视图结构中£¬可能只加载了 ?AdapterView? 子控件的一部分£¬ 简单的 ?onview()? 搜索不能找到当前没有被加载的视图¡£Espresso 通过提供单独的 onData()? 切入点处理此问题£¬它可以在操作适配器中有?#26790;?#39064;的条目或该条目的子项之前将其加载£¨使其获取焦点£©¡£

        注意£º
        你可能?#25442;?#23545;初始状态就?#20801;?#22312;屏幕上的适配器条目执行 ?onData()? 加载操作£¬因为它?#19988;?#32463;被加载了¡£?#27426;ø£?#19968;直使用 ?onData()? 会更安全¡£

        使用 onData 编写一个简单的测试

        SimpleActivity? 包含一个 ?Spinner? £¬该 Spinner? 中有几个条目——代表咖啡类型的字符串¡£当选中其中一个条目时£¬?TextView? 内容会变成 ?“One %s a day!”?£¬其中 %s 代表选中的条目¡£此测试的目标是打开 ?Spinner?£¬选中一个条目然后验证 ?TextView? 中包含该条目¡£由于 ?Spinner? 类基于 ?AdapterView?£¬建议使用 ?onData()? 而不是 ?onView()? 来匹配条目¡£

        • 点击 Spinner 打开条目选择框

        onView(withId(R.id.spinner_simple)).perform(click());
        • 点击 “Americano” 条目
          为了条目可供选择£¬Spinner 用它的内容创建了一个 ?ListView?¡£该 ListView 可能会很长£¬而且它的元素?#25442;?#20986;现在视图结构中¡£通过使用 ?onData()? 我们强制将想要得到的元素加入到视图结构中¡£Spinner 中的元素是字符串£¬我们想要匹配的条目是字符串类型并且值是 “Americano”¡£

        onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());
        • 验证 TextView? 包含 “Americano” 字符串

        onView(withId(R.id.spinnertext_simple).check(matches(withText(containsString("Americano"))));

        自定义ListView匹配器事例£º

        public static Matcher<? super Object> withContactTitle(final String title) {
                return new BoundedMatcher(Contact.class) {
        
                    @Override
                    public void describeTo(Description description) {
                        description.appendText("with id:"+title);
                    }
        
                    @Override
                    protected boolean matchesSafely(Object item) {
                        if(item instanceof Contact){
                            return title.equals(((Contact)item).getFullName());
                        }
                        return false;
                    }
                };
            }

        测试Menu菜单

        分两种情况£º1¡¢菜单按钮?#20801;?#22312;title上£¬2¡¢菜单隐藏在pop中

        1¡¢菜单?#20801;?#22312;titlebar中的测试方法£º

        onView(allOf(withId(R.id.action_create),withContentDescription("创建")))
                .check(matches(isDisplayed())).perform(click());
        onView(withText("创建")).check(matches(isDisplayed()));
        onView(allOf(withId(R.id.action_search),withContentDescription("搜索")))
                .check(matches(isDisplayed())).perform(click());
        onView(withText("搜索")).check(matches(isDisplayed()));

        说明£º不论菜单在titleBar上?#20801;¾icon£¬还是?#20801;?#25991;字£¬菜单布局中都必须包含title£¬用作菜单描述£¬否则没法定位对应的菜单项¡£
        当菜单只?#20801;¾icon时£¬请使用withContentDescription(“title”)定位对应的菜单£¬withText(“title")不生效£¬原因是菜单中压根没有设置title
        详情查看ActionMenuItemView类

        2¡¢ 菜单隐藏在pop中£º
        首先需要打开pop

        //打开menu
        openContextualActionModeOverflowMenu();
        onView(allOf(withId(R.id.title),withText("创建"))).check(matches(isDisplayed())).perform(click());

        注明£º首先需要打开menu菜单框£¬title为菜单文本对应的TextView的id£¬此处固定为title£¬详情请查看ListMenuItemView类

        针对RecycleView测试方法

        RecyclerView 是一个像 ListView¡¢GridVIew 那样呈现数据集合的 UI 组件£¬?#23548;?#19978;它的目的是要替换掉这两个组件¡£从测试的角度上来看我们?#34892;?#36259;的就是 RecyclerView 不是一个 AdapterView£¬这意味着你不能使用 onData() 去跟你的 list items ?#25442;¥¡?/p>

        ?#20197;?#30340;是£¬有一个叫 RecyclerViewActions 的类提供了简单的 API 给我们操作 RecyclerView¡£RecyclerViewActions 是 espresso-contrib库的一部分£¬这个库的?#35272;?#21487;以在 build.gradle 中添加£º

        androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.0');

        这个时候就需要引用到一个 RecyclerViewActions £¬RecyclerViewActions就是为了针对RecyclerView才出来的¡£ 我们主要还是看看如何进行测试吧¡£

        • 点击RecyclerView列表中第1个item

        onView(withId(R.id.pull_refresh_list)).perform(RecyclerViewActions.actionOnItemAtPosition(1,click()));
        • 点击带有 “Effective Java ” 字符串的item

        onView(withId(R.id.pull_refresh_list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(taskName)),click()));

        这里的hasDescendant 指代的是对应的item的后代中有包含对应文本的内容的¡£不过使用这个需要小心 因为很有可能会出现两个同样内容的¡£

        • actionOnHolderItem 的用法

        @Test
        public void testItemSelect() {
            onView(withId(R.id.pull_refresh_list))
                    .perform(RecyclerViewActions.actionOnHolderItem(
                            new CustomViewHolderMatcher(hasDescendant(withText("Effective Java "))), click()));
        
        }
        
        private static class CustomViewHolderMatcher extends TypeSafeMatcher<RecyclerView.ViewHolder> {
            private Matcher<View> itemMatcher = any(View.class);
        
            public CustomViewHolderMatcher() { }
        
            public CustomViewHolderMatcher(Matcher<View> itemMatcher) {
                this.itemMatcher = itemMatcher;
            }
        
            @Override
            public boolean matchesSafely(RecyclerView.ViewHolder viewHolder) {
                return TaskListAdapter.ViewHolder.class.isAssignableFrom(viewHolder.getClass())
                        &amp;&amp; itemMatcher.matches(viewHolder.itemView);
            }
        
            @Override
            public void describeTo(Description description) {
                description.appendText("is assignable from CustomViewHolder");
            }
        }

         

        Espress线程同步问题

        使用 registerIdlingResource 与自定义?#35797;?#21516;步

        Espresso 的核心是它可以与待测应用无缝同步测试操作的能力¡£默认情况下£¬Espresso 会等待当前消息队列中的 UI ?#24405;?#25191;行£¨默认是 AsyncTask£©完毕再进行下一个测试操作¡£这应该能解决大部分应用与测试同步的问题¡£

        ?#27426;ø£?#24212;用中有一些执行后台操作的对象£¨比如与网络服务?#25442;¥£?#36890;过非标准方式实现£»例如£º直接创建和管理线程£¬以及使用自定义服务¡£

        此种情况£¬我们建议你首先提出可测试性的概念£¬然后询问使用非标准后台操作是否必要¡£某些情况下£¬可能是由于对 Android 理解太少造成的£¬并且应用也会受益于重构£¨例如£¬将自定义创建的线程改为 AsyncTask£©¡£?#27426;ø£?#26576;些时候重构并不现实¡£庆幸的是 Espresso 仍然可以同步测试操作与你的自定义?#35797;础?/p>

        以下是我们需要完成的£º

        • 实现 ?IdlingResource? 接口并暴露给测试¡£

        • 通过在 setUp 中调用 ?Espresso.registerIdlingResource? 注册一个或多个 IdlingResource 给 Espresso¡£

        需要注意的是 IdlingResource 接口是在待测应用中实现的£¬所以你需要谨慎的添加?#35272;“ú?/p>

        // IdlingResource is used in the app under test
        compile 'com.android.support.test.espresso:espresso-idling-resource:2.2.2'

        例如?#21512;?#30446;使用OkHttp库£¬此时需要定制针对OKHttp的同步测试£¬代码如下£º

        public final class OkHttp3IdlingResource implements IdlingResource {
            /**
             * Create a new {@link IdlingResource} from {@code client} as {@code name}. You must register
             * this instance using {@code Espresso.registerIdlingResources}.
             */
            @CheckResult @NonNull
            @SuppressWarnings("ConstantConditions") // Extra guards as a library.
            public static OkHttp3IdlingResource create(@NonNull String name, @NonNull OkHttpClient client) {
                if (name == null) throw new NullPointerException("name == null");
                if (client == null) throw new NullPointerException("client == null");
                return new OkHttp3IdlingResource(name, client.dispatcher());
            }
        
            private final String name;
            private final Dispatcher dispatcher;
            volatile ResourceCallback callback;
        
            private OkHttp3IdlingResource(String name, Dispatcher dispatcher) {
                this.name = name;
                this.dispatcher = dispatcher;
                dispatcher.setIdleCallback(new Runnable() {
                    @Override
                    public void run() {
                        ResourceCallback callback = OkHttp3IdlingResource.this.callback;
                        if (callback != null) {
                            callback.onTransitionToIdle();
                        }
                    }
                });
            }
        
            @Override
            public String getName() {
                return name;
            }
        
            @Override
            public boolean isIdleNow() {
                return dispatcher.runningCallsCount() == 0;
            }
        
            @Override
            public void registerIdleTransitionCallback(ResourceCallback callback) {
                this.callback = callback;
            }
        }
        
        //测试代码中加入以下代码块
        @Before
        public void registerIdlingResource(){
            okHttp3IdlingResource = OkHttp3IdlingResource.create("okhttp",HaizhiRestClient.getHttpClient());
            Espresso.registerIdlingResources(okHttp3IdlingResource);
               
        }
        @After
        public void unregisterIdlingResource(){
            Espresso.unregisterIdlingResources(okHttp3IdlingResource);
        }

        Espresso-Intents

        Espresso-Intents 是 Espresso 的一个扩展£¬它使验证和存根待测应用向外发出的意图成为可能¡£它类似于 Mockito£¬但是针对的是 Android 的意图£¨专门针对Android的intent的扩展£©¡£
        Espresso-Intents 只兼容 Espresso 2.1+ 和 testing support library 0.3

        在应用的build.gradle文件中添加以下配置

        androidTestCompile ('com.android.support.test.espresso:espresso-intents:2.2.2'){
            exclude group: 'com.android.support', module: 'support-annotations'
        }

        IntentsTestRule

        使用 Espresso-Intents 时£¬应当用 ?IntentsTestRule? 替换 ?ActivityTestRule?¡£IntentsTestRule? 使得在 UI 功能测试中使用 Espresso-Intents API 变得简单¡£该类是 ?ActivityTestRule? 的扩展£¬它会在每一个被 [email protected]? 注解的测试执行前初始化 Espresso-Intents£¬然后在测试执行完后释放 Espresso-Intents¡£被启动的 activity 会在每个测试执行完后被终止掉£¬此规则也适用于 ?ActivityTestRule?¡£

        验证意图£¨Intent validation£©

        Espresso-Intents 会记录待测应用里所有尝试启动 Activity 意图¡£使用 intended API£¨与 ?Mockito.verify? 类似£©你可以断言特定的意图是否被发出¡£

        验证外发意图的简单示例£º

        onView(withText("send")).perform(click());
        //验证发送短信界面成功调用
        Uri smsToUri = Uri.parse("smsto:10086");
        intended(hasData(smsToUri));

        意图存根£¨Intent stubbing£©

        使用 intending API£¨与 ?Mockito.when? 类似£©你可以为通过 startActivityForResult 启动的 Activity 提供一个响应结果£¨尤其是外部的 Activity£¬因为我们不能操作外部 activity 的用户界面£¬也不能控制 ?ActivityResult? 返回给待测 Activity£©¡£

        使用意图存根的示例£º

        @Test
        public void startSecondActivity(){
             Intent intent = new Intent();
             intent.putExtra("test","test");
             Instrumentation.ActivityResult result =
                        new Instrumentation.ActivityResult(Activity.RESULT_OK,intent);
        //        intending(anyIntent()).respondWith(result);
                //必须使用完整类名(包名+类名)
            intending(hasComponent(InstrumentationRegistry.getTargetContext().getPackageName()+"."+
                        SecondActivity.class.getSimpleName())).respondWith(result);
                onView(withText("start")).perform(click());
                onView(withText("test")).check(matches(isDisplayed()));
        }

        说明£º从MainActivity中点击按钮start跳转到SecondActivity中£¬返回时带回参数Test ?#20801;?#21040;MainActivity的界面中¡£

        意图匹配器£¨Intent Matchers£©
        intending? 和 ?intended? 方法用一个 hamcrest ?Matcher<Intent>? 作为参数¡£ Hamcrest 是匹配器对象£¨也称为约束或断言£©库¡£有以下选项£º

        • 使用现有的意图匹配器£º最简单的选择£¬绝大多数情况的首选¡£

        • 自己实现意图匹配器£¬最灵活的选择£¨参考 Hamcrest 教程 的 “Writing custom matchers” 章节£©

        以下是一个使用现有的意图匹配器验证意图的示例£º

        intended(allOf(
            hasAction(equalTo(Intent.ACTION_VIEW)),
            hasCategories(hasItem(equalTo(Intent.CATEGORY_BROWSABLE))),
            hasData(hasHost(equalTo("www.google.com"))),
            hasExtras(allOf(
                hasEntry(equalTo("key1"), equalTo("value1")),
                hasEntry(equalTo("key2"), equalTo("value2")))),
                toPackage("com.android.browser")));

        Mock

        mock的概念其实很简单£¬所谓的mock就是创建一个类的虚拟对象£¬在测试环境中用来替换掉真是的对象£¬?#28304;?#21040;两个目的£º

        • 验证这个对象的某些方法的调用情况£¬调用了多少次£¬参数是什么等等¡£

        • 指定这个对象的某些方法的行为£¬返回特定的值£¬或者是执行特定的动作¡£

        Mockito

        要是用mock一般需要使用到mock框架£¬Mockito框架是java界使用最广泛的一个mock框架¡£

        1¡¢申明?#35272;?/p>

        testCompile 'junit:junit:4.12'
        testCompile 'org.mockito:mockito-core:1.+'
        // 如果你要使用Mockito 用于 Android instrumentation tests£¬那么需要你添加以下三条?#35272;?#24211;
        androidTestCompile 'org.mockito:mockito-core:1.+'
        androidTestCompile "com.google.dexmaker:dexmaker:1.2"
        androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"

        2¡¢创建Mock对象

        在 Mockito 中你可以使用 mock() 方法来创建一个模拟对象£¬?#37096;?#20197;使用注解的方式 @Mock 来创建 £¬这里推荐使用注解¡£需要注意的是£¬如果是使用注解方式£¬需要在使用前进行初始化¡£

        使用注解方式创建有三种初始化方式£º

        1£©¡¢使用 MockitoAnnotations.initMocks(this) 方式

        public class MockitoAnnotationsTest {
            @Mock
            AccountData accountData;
            @Before
            public void setupAccountData(){
                MockitoAnnotations.initMocks(this);
            }
            @Test
            public void testIsNotNull(){
                assertNotNull(accountData);
            }
        }

        2£©¡¢使用 @RunWith(MockitoJUnitRunner.class) 方式

        @RunWith(MockitoJUnitRunner.class)
        public class MockitoJUnitRunnerTest {
            @Mock
            AccountData accountData;
        
            @Test
            public void testIsNotNull() {
                assertNotNull(accountData);
            }
        }

        3£©¡¢使用 MockitoRule 方式

        public class MockitoRuleTest {
            @Mock
            AccountData accountData;
            @Rule
            public MockitoRule mockitoRule = MockitoJUnit.rule();
        
            @Test
            public void testIsNotNull(){
                assertNotNull(accountData);
            }
        }

        使用mock方式创建mock对象

        public class MockitoTest {
        
            AccountData accountData;
            @Before
            public void setup(){
               accountData = Mockito.mock(AccountData.class);
            }
            @Test
            public void testIsNotNull(){
                assertNotNull(accountData);
            }
        }

        说明£º

        • Mockito.mock() 并不是mock一整个类£¬而是根据传进去的一个类£¬mock出属于这个类的一个对象£¬并且返回这个 mock对象?#27426;?#20256;进去的这个类本身并没有改变£¬用这个类new出来的对象也没有受到任何改变£¡

        • Mockito.verify() 的参数必须是mock对象£¬也就是说£¬Mockito只能验证mock对象的方法调用情况

        Mockito的使用

        1¡¢验证方法的调用

        前面我们讲?#25628;?#35777;一个对象的某个method得到调用的方法£º

        Mockito.verify(accountData).isLogin();

        这行代码验证的是£¬ accountData 的 isLogin() 方法得到了 一次 调用¡£因为这行代码其实是£º

        Mockito.verify(accountData, Mockito.times(1)).isLogin();

        因此£¬如果你想验证一个对象的某个方法得到了多次调用£¬只需要将次数传给 Mockito.times() 就好了¡£

        Mockito.verify(accountData, Mockito.times(3)).isLogin(); //accountData的isLogin方法调用了3次¡£

        对于调?#20040;?#25968;的验证£¬除了可以验证固定的多少次£¬还可以验证最多£¬最少从来没有等等£¬方法分别是£º

        • Mockito.verify() : 验证Mock对象的方法是否被调用¡£

        • Mockito.times() : 调用mock对象的次数

        • Mockito.atMost(count) , Mockito.atLeast(count) , Mockito.never() £º最多次数£¬最少次数£¬永远调用¡£

        • Mockito.anyInt() , Mockito.anyLong() , Mockito.anyDouble()等等 £º 参数设置-?#25105;?#30340;Int类?#20572;¬È我?#30340;Long类型¡£¡£¡£等¡£

        • anyCollection£¬anyCollectionOf(clazz), anyList(Map, set), anyListOf(clazz)

        2¡¢指定mock对象的某些方法的行为

        到目前为止£¬我们介绍了mock的一大作用£º验证方法调用¡£我们说mock主要有两大作用£¬第二个大作用是£º指定某个方法的返回值£¬或者是执行特定的动作¡£
        那么接下来£¬我们就来介绍mock的第二大作用£¬先介绍其中的第一点£º指定mock对象的某个方法返回特定的值¡£

        //希望 isLogin() 方法被调用时返回true£¬那么你可以这样写£º

        when(accountData.isLogin()).thenReturn(true);
        //验证结果
        boolean islogin = accountData.isLogin();
        assertTrue(islogin);
        
        //如果你希望 getUserName() 被调用返回Jack
        when(accountData.getUserName()).thenReturn("Jack");
        assertEquals("Jack",accountData.getUserName());
        
        //如果你希望对 setUserName(String userName) 方法中参数进行测试
        accountData.setUserName("haha");
        verify(accountData).setUserName(Matchers.eq("haha"));

        同样的£¬你可以用 any 系列方法来指定"无论传入任何参数值£¬都返回xxx"£º

        //当调用accountData的setUserName1方法时£¬返回haha£¬无论参数是什么
        when(accountData.setUserName1(anyString())).thenReturn("haha");

        在这里£¬我们想进一步测试传给 accountData.login() 的 NetworkCallback 里面的代码£¬验证view得到了更新等等¡£在测试环境下£¬我们并不想?#35272;?accountData.login() 的真实逻辑£¬而是让 accountData.login 直接调?#20040;?#20837;的 NetworkCallback 的 onSuccess 或 onFailure 方法¡£这种指定mock对象执行特定的动作的写法如下£º

        Mockito.doAnswer(desiredAnswer).when(mockObject).targetMethod(args);

        传给 doAnswer() 的是一个 Answer 对象£¬我们想要执行什么样的动作£¬就在这里面实现¡£结合上面的

        @Test
        public void test_login(){
            activityTestRule.getActivity().setAccountData(accountData);
            doAnswer(new Answer() {
                @Override
                public Object answer(InvocationOnMock invocation) throws Throwable {
        
                    //这里可以获取传给login的参数
                    Object[] args = invocation.getArguments();
        
                    //callback是第三个参数
                    NetWorkCallBack callBack = (NetWorkCallBack) args[2];
        
                    callBack.onFailure(500,"Server error");
                    return 500;
                }
            }).when(accountData).login(anyString(),anyString(),any(NetWorkCallBack.class));
        
            onView(withText("button")).perform(click());
            onView(withText("code=500:errorMsg=Server error")).check(matches(isDisplayed()));
        }

        Spy

        前面我们讲了mock对象的两大功能£¬对于第二大功能: 指定方法的特定行为£¬不知道你会?#25442;?#22909;奇£¬如果我不指定的话£¬它会怎么样呢£¿那么现在补充一下£¬如果不指定的话£¬一个mock对象的所有非void方法都将返回默?#29616;“úºint¡¢long类型方法将返回0£¬boolean方法将返回false£¬对象方法将返回null等等?#27426;øvoid方法将什么都不做¡£?#27426;?#24456;多时候£¬你希望达到这样的效果£º除非指定£¬否者调用这个对象的默认实现£¬同时又能拥有验证方法调用的功能¡£这正好是spy对象所能实现的效果¡£创建一个spy对象£¬以及spy对象的用法介绍如下£º

        //假设目标类的实现是这样的
        public class PasswordValidator {
            public boolean verifyPassword(String password) {
                return "test_spy".equals(password);
            }
        }
        
        @RunWith(MockitoJUnitRunner.class)
        public class SpyTest {
            @Spy
            PasswordValidator passwordValidator;
        
            @Test
            public void test_verifyPassword(){
                //跟创建mock类似£¬只不过调用的是spy方法£¬而不是mock方法¡£spy的用法
                Assert.assertTrue(passwordValidator.verifyPassword("test_spy"));
                Assert.assertFalse(passwordValidator.verifyPassword("test_spy1"));
        
                //spy对象的方法?#37096;?#20197;指定特定的行为
                when(passwordValidator.verifyPassword(anyString())).thenReturn(true);
        
                Assert.assertTrue(passwordValidator.verifyPassword("test_spy12"));
                //同样的£¬可以验证spy对象的方法调用情况
                verify(passwordValidator).verifyPassword("test_spy12");
            }
        }

        总之£¬spy与mock的唯一区别就是默认行为不一样£ºspy对象的方法默?#31995;?#29992;真实的逻辑£¬mock对象的方法默认什么都不做£¬或直接返回默?#29616;µ¡?/p>

        Android真机使用Mockito-1.10.19+Dexmaker-1.2在Mock?#22363;?#25277;象父类的子类时报告错误“java.lang.AbstractMethodError: abstract method not implemented”

        在项目的 build.gradle中的声明如下£º

        androidTestCompile 'org.mockito:mockito-core:1.+'
        androidTestCompile "com.google.dexmaker:dexmaker:1.2"
        androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"

        这个问题只在 Dalvik虚拟机下面发生异常£¬相同的代码在 ART下面是完全正常的¡£
        导致问题发生的原因是 Google提供的 dexmaker库存在 BUG导致的£¬而这个库£¬从 Maven Center上看£¬自从 2012年开始就没有提供过任何的更新了¡£
        解决方法是不使用 Google提供的 dexmaker£¬而是使用com.crittercism.dexmaker修正过这个 BUG的版本¡£

        androidTestCompile 'org.mockito:mockito-core:1.10.19'
        androidTestCompile 'com.crittercism.dexmaker:dexmaker:1.4'
        androidTestCompile "com.crittercism.dexmaker:dexmaker-dx:1.4"
        androidTestCompile 'com.crittercism.dexmaker:dexmaker-mockito:1.4'
        Android开发中的自动化测试转载

        快速集成MQTT协议到Android客户端,只需要简

        android自定义尺子收集demo

        错误运行app: This version of Android Stu

        android日历收集demo

        android图片上传功能实现

        安卓开发?#22987;Ç¡ª¡ªTabHost组件(一)£¨实现底

        ¡¾Android¡¿BugHD-简单实用的Bug收集工具-

        Android开发轻松自制flyme悬浮球

        解决关于SearchView的样式与控制问题 Andro

        最完整的Android开发工具集合整理

        Android开发greenDao数据库升级Hepler£¬保

        Android drawText 在指定位置进行画文字

        ´ó¸»ÎÌÆåÅÆÓéÀÖ¹ÙÍø

      6. <code id="58gs9"></code>

          <mark id="58gs9"></mark>

          1. <code id="58gs9"></code>

              <mark id="58gs9"></mark>