<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>debugtalk (九毫)</title>
    <link>https://ruby-china.org/debugtalk</link>
    <description>大道至简</description>
    <language>en-us</language>
    <item>
      <title>深入浅出开源性能测试工具 Locust (使用篇 1)</title>
      <description>&lt;p&gt;在&lt;a href="http://debugtalk.com/post/locustplus-talk-about-performance-test/" rel="nofollow" target="_blank" title=""&gt;《【LocustPlus 序】漫谈服务端性能测试》&lt;/a&gt;中，我对服务端性能测试的基础概念和性能测试工具的基本原理进行了介绍，并且重点推荐了&lt;code&gt;Locust&lt;/code&gt;这一款开源性能测试工具。然而，当前在网络上针对&lt;code&gt;Locust&lt;/code&gt;的教程极少，不管是中文还是英文，基本都是介绍安装方法和简单的测试案例演示，但对于较复杂测试场景的案例演示却基本没有，因此很多测试人员都感觉难以将&lt;code&gt;Locust&lt;/code&gt;应用到实际的性能测试工作当中。&lt;/p&gt;

&lt;p&gt;经过一段时间的摸索，包括通读&lt;code&gt;Locust&lt;/code&gt;官方文档和项目源码，并且在多个性能测试项目中对&lt;code&gt;Locust&lt;/code&gt;进行应用实践，事实证明，&lt;code&gt;Locust&lt;/code&gt;完全能满足日常的性能测试需求，&lt;code&gt;LoadRunner&lt;/code&gt;能实现的功能&lt;code&gt;Locust&lt;/code&gt;也基本都能实现。&lt;/p&gt;

&lt;p&gt;本文将从&lt;code&gt;Locust&lt;/code&gt;的功能特性出发，结合实例对&lt;code&gt;Locust&lt;/code&gt;的使用方法进行介绍。考虑到大众普遍对&lt;code&gt;LoadRunner&lt;/code&gt;比较熟悉，在讲解&lt;code&gt;Locust&lt;/code&gt;时也会采用&lt;code&gt;LoadRunner&lt;/code&gt;的一些概念进行类比。&lt;/p&gt;
&lt;h2 id="概述"&gt;概述&lt;/h2&gt;
&lt;p&gt;先从&lt;code&gt;Locust&lt;/code&gt;的名字说起。&lt;code&gt;Locust&lt;/code&gt;的原意是蝗虫，原作者之所以选择这个名字，估计也是听过这么一句俗语，“蝗虫过境，寸草不生”。我在网上找了张图片，大家可以感受下。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/14875962785342.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;而&lt;code&gt;Locust&lt;/code&gt;工具生成的并发请求就跟一大群蝗虫一般，对我们的被测系统发起攻击，以此检测系统在高并发压力下是否能正常运转。&lt;/p&gt;

&lt;p&gt;在&lt;a href="http://debugtalk.com/post/locustplus-talk-about-performance-test/" rel="nofollow" target="_blank" title=""&gt;《【LocustPlus 序】漫谈服务端性能测试》&lt;/a&gt;中说过，服务端性能测试工具最核心的部分是压力发生器，而压力发生器的核心要点有两个，一是真实模拟用户操作，二是模拟有效并发。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;Locust&lt;/code&gt;测试框架中，测试场景是采用纯 Python 脚本进行描述的。对于最常见的&lt;code&gt;HTTP(S)&lt;/code&gt;协议的系统，&lt;code&gt;Locust&lt;/code&gt;采用 Python 的&lt;code&gt;requests&lt;/code&gt;库作为客户端，使得脚本编写大大简化，富有表现力的同时且极具美感。而对于其它协议类型的系统，&lt;code&gt;Locust&lt;/code&gt;也提供了接口，只要我们能采用 Python 编写对应的请求客户端，就能方便地采用&lt;code&gt;Locust&lt;/code&gt;实现压力测试。从这个角度来说，&lt;code&gt;Locust&lt;/code&gt;可以用于压测任意类型的系统。&lt;/p&gt;

&lt;p&gt;在模拟有效并发方面，&lt;code&gt;Locust&lt;/code&gt;的优势在于其摒弃了进程和线程，完全基于事件驱动，使用&lt;code&gt;gevent&lt;/code&gt;提供的&lt;code&gt;非阻塞IO&lt;/code&gt;和&lt;code&gt;coroutine&lt;/code&gt;来实现网络层的并发请求，因此即使是单台压力机也能产生数千并发请求数；再加上对分布式运行的支持，理论上来说，&lt;code&gt;Locust&lt;/code&gt;能在使用较少压力机的前提下支持极高并发数的测试。&lt;/p&gt;
&lt;h2 id="脚本编写"&gt;脚本编写&lt;/h2&gt;
&lt;p&gt;编写&lt;code&gt;Locust&lt;/code&gt;脚本，是使用&lt;code&gt;Locust&lt;/code&gt;的第一步，也是最为重要的一步。&lt;/p&gt;
&lt;h3 id="简单示例"&gt;简单示例&lt;/h3&gt;
&lt;p&gt;先来看一个最简单的示例。&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;locust&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HttpLocust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebsiteTasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;123456&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;about&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/about/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebsiteUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpLocust&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;task_set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebsiteTasks&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://debugtalk.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;min_wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;max_wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个示例中，定义了针对&lt;code&gt;http://debugtalk.com&lt;/code&gt;网站的测试场景：先模拟用户登录系统，然后随机地访问首页（&lt;code&gt;/&lt;/code&gt;）和关于页面（&lt;code&gt;/about/&lt;/code&gt;），请求比例为&lt;code&gt;2:1&lt;/code&gt;；并且，在测试过程中，两次请求的间隔时间为&lt;code&gt;1~5&lt;/code&gt;秒间的随机值。&lt;/p&gt;

&lt;p&gt;那么，如上 Python 脚本是如何表达出以上测试场景的呢？&lt;/p&gt;

&lt;p&gt;从脚本中可以看出，脚本主要包含两个类，一个是&lt;code&gt;WebsiteUser&lt;/code&gt;（继承自&lt;code&gt;HttpLocust&lt;/code&gt;，而&lt;code&gt;HttpLocust&lt;/code&gt;继承自&lt;code&gt;Locust&lt;/code&gt;），另一个是&lt;code&gt;WebsiteTasks&lt;/code&gt;（继承自&lt;code&gt;TaskSet&lt;/code&gt;）。事实上，在&lt;code&gt;Locust&lt;/code&gt;的测试脚本中，所有业务测试场景都是在&lt;code&gt;Locust&lt;/code&gt;和&lt;code&gt;TaskSet&lt;/code&gt;两个类的继承子类中进行描述的。&lt;/p&gt;

&lt;p&gt;那如何理解&lt;code&gt;Locust&lt;/code&gt;和&lt;code&gt;TaskSet&lt;/code&gt;这两个类呢？&lt;/p&gt;

&lt;p&gt;简单地说，&lt;code&gt;Locust类&lt;/code&gt;就好比是一群蝗虫，而每一只蝗虫就是一个类的实例。相应的，&lt;code&gt;TaskSet类&lt;/code&gt;就好比是蝗虫的大脑，控制着蝗虫的具体行为，即实际业务场景测试对应的任务集。&lt;/p&gt;

&lt;p&gt;这个比喻可能不是很准确，接下来，我将分别对&lt;code&gt;Locust&lt;/code&gt;和&lt;code&gt;TaskSet&lt;/code&gt;两个类进行详细介绍。&lt;/p&gt;
&lt;h3 id="class HttpLocust(Locust)"&gt;class HttpLocust(Locust)&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;Locust类&lt;/code&gt;中，具有一个&lt;code&gt;client&lt;/code&gt;属性，它对应着虚拟用户作为客户端所具备的请求能力，也就是我们常说的请求方法。通常情况下，我们不会直接使用&lt;code&gt;Locust&lt;/code&gt;类，因为其&lt;code&gt;client&lt;/code&gt;属性没有绑定任何方法。因此在使用&lt;code&gt;Locust&lt;/code&gt;时，需要先继承&lt;code&gt;Locust类&lt;/code&gt;，然后在继承子类中的&lt;code&gt;client&lt;/code&gt;属性中绑定客户端的实现类。&lt;/p&gt;

&lt;p&gt;对于常见的&lt;code&gt;HTTP(S)&lt;/code&gt;协议，&lt;code&gt;Locust&lt;/code&gt;已经实现了&lt;code&gt;HttpLocust&lt;/code&gt;类，其&lt;code&gt;client&lt;/code&gt;属性绑定了&lt;code&gt;HttpSession&lt;/code&gt;类，而&lt;code&gt;HttpSession&lt;/code&gt;又继承自&lt;code&gt;requests.Session&lt;/code&gt;。因此在测试&lt;code&gt;HTTP(S)&lt;/code&gt;的&lt;code&gt;Locust脚本&lt;/code&gt;中，我们可以通过&lt;code&gt;client&lt;/code&gt;属性来使用&lt;code&gt;Python requests&lt;/code&gt;库的所有方法，包括&lt;code&gt;GET/POST/HEAD/PUT/DELETE/PATCH&lt;/code&gt;等，调用方式也与&lt;code&gt;requests&lt;/code&gt;完全一致。另外，由于&lt;code&gt;requests.Session&lt;/code&gt;的使用，因此&lt;code&gt;client&lt;/code&gt;的方法调用之间就自动具有了状态记忆的功能。常见的场景就是，在登录系统后可以维持登录状态的&lt;code&gt;Session&lt;/code&gt;，从而后续 HTTP 请求操作都能带上登录态。&lt;/p&gt;

&lt;p&gt;而对于&lt;code&gt;HTTP(S)&lt;/code&gt;以外的协议，我们同样可以使用&lt;code&gt;Locust&lt;/code&gt;进行测试，只是需要我们自行实现客户端。在客户端的具体实现上，可通过注册事件的方式，在请求成功时触发&lt;code&gt;events.request_success&lt;/code&gt;，在请求失败时触发&lt;code&gt;events.request_failure&lt;/code&gt;即可。然后创建一个继承自&lt;code&gt;Locust类&lt;/code&gt;的类，对其设置一个&lt;code&gt;client&lt;/code&gt;属性并与我们实现的客户端进行绑定。后续，我们就可以像使用&lt;code&gt;HttpLocust类&lt;/code&gt;一样，测试其它协议类型的系统。&lt;/p&gt;

&lt;p&gt;原理就是这样简单！&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;Locust类&lt;/code&gt;中，除了&lt;code&gt;client&lt;/code&gt;属性，还有几个属性需要关注下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;task_set&lt;/code&gt;: 指向一个&lt;code&gt;TaskSet&lt;/code&gt;类，&lt;code&gt;TaskSet&lt;/code&gt;类定义了用户的任务信息，该属性为必填；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_wait/min_wait&lt;/code&gt;: 每个用户执行两个任务间隔时间的上下限（毫秒），具体数值在上下限中随机取值，若不指定则默认间隔时间固定为 1 秒；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;host&lt;/code&gt;：被测系统的 host，当在终端中启动&lt;code&gt;locust&lt;/code&gt;时没有指定&lt;code&gt;--host&lt;/code&gt;参数时才会用到；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;weight&lt;/code&gt;：同时运行多个&lt;code&gt;Locust类&lt;/code&gt;时会用到，用于控制不同类型任务的执行权重。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;测试开始后，每个虚拟用户（&lt;code&gt;Locust实例&lt;/code&gt;）的运行逻辑都会遵循如下规律：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;先执行&lt;code&gt;WebsiteTasks&lt;/code&gt;中的&lt;code&gt;on_start&lt;/code&gt;（只执行一次），作为初始化；&lt;/li&gt;
&lt;li&gt;从&lt;code&gt;WebsiteTasks&lt;/code&gt;中随机挑选（如果定义了任务间的权重关系，那么就是按照权重关系随机挑选）一个任务执行；&lt;/li&gt;
&lt;li&gt;根据&lt;code&gt;Locust类&lt;/code&gt;中&lt;code&gt;min_wait&lt;/code&gt;和&lt;code&gt;max_wait&lt;/code&gt;定义的间隔时间范围（如果&lt;code&gt;TaskSet类&lt;/code&gt;中也定义了&lt;code&gt;min_wait&lt;/code&gt;或者&lt;code&gt;max_wait&lt;/code&gt;，以&lt;code&gt;TaskSet&lt;/code&gt;中的优先），在时间范围中随机取一个值，休眠等待；&lt;/li&gt;
&lt;li&gt;重复&lt;code&gt;2~3&lt;/code&gt;步骤，直至测试任务终止。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="class TaskSet"&gt;class TaskSet&lt;/h3&gt;
&lt;p&gt;再说下&lt;code&gt;TaskSet类&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;性能测试工具要模拟用户的业务操作，就需要通过脚本模拟用户的行为。在前面的比喻中说到，&lt;code&gt;TaskSet类&lt;/code&gt;好比蝗虫的大脑，控制着蝗虫的具体行为。&lt;/p&gt;

&lt;p&gt;具体地，&lt;code&gt;TaskSet类&lt;/code&gt;实现了虚拟用户所执行任务的调度算法，包括规划任务执行顺序（&lt;code&gt;schedule_task&lt;/code&gt;）、挑选下一个任务（&lt;code&gt;execute_next_task&lt;/code&gt;）、执行任务（&lt;code&gt;execute_task&lt;/code&gt;）、休眠等待（&lt;code&gt;wait&lt;/code&gt;）、中断控制（&lt;code&gt;interrupt&lt;/code&gt;）等等。在此基础上，我们就可以在&lt;code&gt;TaskSet&lt;/code&gt;子类中采用非常简洁的方式来描述虚拟用户的业务测试场景，对虚拟用户的所有行为（任务）进行组织和描述，并可以对不同任务的权重进行配置。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;TaskSet&lt;/code&gt;子类中定义任务信息时，可以采取两种方式，&lt;code&gt;@task装饰器&lt;/code&gt;和&lt;code&gt;tasks属性&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;采用&lt;code&gt;@task装饰器&lt;/code&gt;定义任务信息时，描述形式如下：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;locust&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserBehavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;采用&lt;code&gt;tasks属性&lt;/code&gt;定义任务信息时，描述形式如下：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;locust&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TaskSet&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserBehavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;test_job1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_job2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;# tasks = [(test_job1,1), (test_job1,2)] # 两种方式等价
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在如上两种定义任务信息的方式中，均设置了权重属性，即执行&lt;code&gt;test_job2&lt;/code&gt;的频率是&lt;code&gt;test_job1&lt;/code&gt;的两倍。&lt;/p&gt;

&lt;p&gt;若不指定执行任务的权重，则相当于比例为&lt;code&gt;1:1&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;locust&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserBehavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;locust&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TaskSet&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserBehavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;test_job1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_job2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# tasks = {test_job1:1, test_job2:1} # 两种方式等价
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;TaskSet&lt;/code&gt;子类中除了定义任务信息，还有一个是经常用到的，那就是&lt;code&gt;on_start&lt;/code&gt;函数。这个和&lt;code&gt;LoadRunner&lt;/code&gt;中的&lt;code&gt;vuser_init&lt;/code&gt;功能相同，在正式执行测试前执行一次，主要用于完成一些初始化的工作。例如，当测试某个搜索功能，而该搜索功能又要求必须为登录态的时候，就可以先在&lt;code&gt;on_start&lt;/code&gt;中进行登录操作；前面也提到，&lt;code&gt;HttpLocust&lt;/code&gt;使用到了&lt;code&gt;requests.Session&lt;/code&gt;，因此后续所有任务执行过程中就都具有登录态了。&lt;/p&gt;
&lt;h3 id="脚本增强"&gt;脚本增强&lt;/h3&gt;
&lt;p&gt;掌握了&lt;code&gt;HttpLocust&lt;/code&gt;和&lt;code&gt;TaskSet&lt;/code&gt;，我们就基本具备了编写测试脚本的能力。此时再回过头来看前面的案例，相信大家都能很好的理解了。&lt;/p&gt;

&lt;p&gt;然而，当面对较复杂的测试场景，可能有的同学还是会感觉无从下手；例如，很多时候脚本需要做关联或参数化处理，这些在&lt;code&gt;LoadRunner&lt;/code&gt;中集成的功能，换到&lt;code&gt;Locust&lt;/code&gt;中就不知道怎么实现了。可能也是这方面的原因，造成很多测试人员都感觉难以将 Locust 应用到实际的性能测试工作当中。&lt;/p&gt;

&lt;p&gt;其实这也跟&lt;code&gt;Locust&lt;/code&gt;的目标定位有关，&lt;code&gt;Locust&lt;/code&gt;的定位就是&lt;code&gt;small and very hackable&lt;/code&gt;。但是小巧并不意味着功能弱，我们完全可以通过 Python 脚本本身来实现各种各样的功能，如果大家有疑问，我们不妨逐项分解来看。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;LoadRunner&lt;/code&gt;这款功能全面应用广泛的商业性能测试工具中，脚本增强无非就涉及到四个方面：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;关联&lt;/li&gt;
&lt;li&gt;参数化&lt;/li&gt;
&lt;li&gt;检查点&lt;/li&gt;
&lt;li&gt;集合点&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;先说关联这一项。在某些请求中，需要携带之前从 Server 端返回的参数，因此在构造请求时需要先从之前请求的 Response 中提取出所需的参数，常见场景就是&lt;code&gt;session_id&lt;/code&gt;。针对这种情况，&lt;code&gt;LoadRunner&lt;/code&gt;虽然可能通过录制脚本进行自动关联，但是效果并不理想，在实际测试过程中也基本都是靠测试人员手动的来进行关联处理。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;LoadRunner&lt;/code&gt;中手动进行关联处理时，主要是通过使用注册型函数，例如&lt;code&gt;web_reg_save_param&lt;/code&gt;，对前一个请求的响应结果进行解析，根据左右边界或其它特征定位到参数值并将其保存到参数变量，然后在后续请求中使用该参数。采用同样的思想，我们在&lt;code&gt;Locust&lt;/code&gt;脚本中也完全可以实现同样的功能，毕竟只是 Python 脚本，通过官方库函数&lt;code&gt;re.search&lt;/code&gt;就能实现所有需求。甚至针对 html 页面，我们也可以采用&lt;code&gt;lxml&lt;/code&gt;库，通过&lt;code&gt;etree.HTML(html).xpath&lt;/code&gt;来更优雅地实现元素定位。&lt;/p&gt;

&lt;p&gt;然后再来看参数化这一项。这一项极其普遍，主要是用在测试数据方面。但通过归纳，发现其实也可以概括为三种类型。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;循环取数据，数据可重复使用：e.g. 模拟 3 用户并发请求网页，总共有 100 个 URL 地址，每个虚拟用户都会依次循环加载这 100 个 URL 地址；&lt;/li&gt;
&lt;li&gt;保证并发测试数据唯一性，不循环取数据：e.g. 模拟 3 用户并发注册账号，总共有 90 个账号，要求注册账号不重复，注册完毕后结束测试；&lt;/li&gt;
&lt;li&gt;保证并发测试数据唯一性，循环取数据：模拟 3 用户并发登录账号，总共有 90 个账号，要求并发登录账号不相同，但数据可循环使用。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;通过以上归纳，可以确信地说，以上三种类型基本上可以覆盖我们日常性能测试工作中的所有参数化场景。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;LoadRunner&lt;/code&gt;中是有一个集成的参数化模块，可以直接配置参数化策略。那在&lt;code&gt;Locust&lt;/code&gt;要怎样实现该需求呢？&lt;/p&gt;

&lt;p&gt;答案依旧很简单，使用 Python 的&lt;code&gt;list&lt;/code&gt;和&lt;code&gt;queue&lt;/code&gt;数据结构即可！具体做法是，在&lt;code&gt;WebsiteUser&lt;/code&gt;定义一个数据集，然后所有虚拟用户在&lt;code&gt;WebsiteTasks&lt;/code&gt;中就可以共享该数据集了。如果不要求数据唯一性，数据集选择&lt;code&gt;list&lt;/code&gt;数据结构，从头到尾循环遍历即可；如果要求数据唯一性，数据集选择&lt;code&gt;queue&lt;/code&gt;数据结构，取数据时进行&lt;code&gt;queue.get()&lt;/code&gt;操作即可，并且这也不会循环取数据；至于涉及到需要循环取数据的情况，那也简单，每次取完数据后再将数据插入到队尾即可，&lt;code&gt;queue.put_nowait(data)&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;最后再说下检查点。该功能在&lt;code&gt;LoadRunner&lt;/code&gt;中通常是使用&lt;code&gt;web_reg_find&lt;/code&gt;这类注册函数进行检查的。在&lt;code&gt;Locust&lt;/code&gt;脚本中，处理就更方便了，只需要对响应的内容关键字进行&lt;code&gt;assert xxx in response&lt;/code&gt;操作即可。&lt;/p&gt;

&lt;p&gt;针对如上各种脚本增强的场景，我也通过代码示例分别进行了演示。但考虑到文章中插入太多代码会影响到阅读，因此将代码示例部分剥离了出来，如有需要请点击查看&lt;a href="http://debugtalk.com/post/head-first-locust-advanced-script/" rel="nofollow" target="_blank" title=""&gt;《深入浅出开源性能测试工具 Locust（脚本增强）》&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="GitHub 项目地址"&gt;GitHub 项目地址&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/debugtalk/Stormer" rel="nofollow" target="_blank"&gt;https://github.com/debugtalk/Stormer&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="硬广"&gt;硬广&lt;/h2&gt;
&lt;p&gt;欢迎关注我的个人博客和微信公众号。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;个人博客：&lt;a href="http://debugtalk.com" rel="nofollow" target="_blank"&gt;http://debugtalk.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;微信公众号：&lt;a href="http://debugtalk.com/images/wechat_qrcode.png" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DebugTalk&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>debugtalk</author>
      <pubDate>Fri, 10 Mar 2017 20:18:21 +0800</pubDate>
      <link>https://ruby-china.org/topics/32502</link>
      <guid>https://ruby-china.org/topics/32502</guid>
    </item>
    <item>
      <title>深入浅出开源性能测试工具 Locust (使用篇 1)</title>
      <description>&lt;p&gt;在&lt;a href="http://debugtalk.com/post/locustplus-talk-about-performance-test/" rel="nofollow" target="_blank" title=""&gt;《【LocustPlus 序】漫谈服务端性能测试》&lt;/a&gt;中，我对服务端性能测试的基础概念和性能测试工具的基本原理进行了介绍，并且重点推荐了&lt;code&gt;Locust&lt;/code&gt;这一款开源性能测试工具。然而，当前在网络上针对&lt;code&gt;Locust&lt;/code&gt;的教程极少，不管是中文还是英文，基本都是介绍安装方法和简单的测试案例演示，但对于较复杂测试场景的案例演示却基本没有，因此很多测试人员都感觉难以将&lt;code&gt;Locust&lt;/code&gt;应用到实际的性能测试工作当中。&lt;/p&gt;

&lt;p&gt;经过一段时间的摸索，包括通读&lt;code&gt;Locust&lt;/code&gt;官方文档和项目源码，并且在多个性能测试项目中对&lt;code&gt;Locust&lt;/code&gt;进行应用实践，事实证明，&lt;code&gt;Locust&lt;/code&gt;完全能满足日常的性能测试需求，&lt;code&gt;LoadRunner&lt;/code&gt;能实现的功能&lt;code&gt;Locust&lt;/code&gt;也基本都能实现。&lt;/p&gt;

&lt;p&gt;本文将从&lt;code&gt;Locust&lt;/code&gt;的功能特性出发，结合实例对&lt;code&gt;Locust&lt;/code&gt;的使用方法进行介绍。考虑到大众普遍对&lt;code&gt;LoadRunner&lt;/code&gt;比较熟悉，在讲解&lt;code&gt;Locust&lt;/code&gt;时也会采用&lt;code&gt;LoadRunner&lt;/code&gt;的一些概念进行类比。&lt;/p&gt;
&lt;h2 id="概述"&gt;概述&lt;/h2&gt;
&lt;p&gt;先从&lt;code&gt;Locust&lt;/code&gt;的名字说起。&lt;code&gt;Locust&lt;/code&gt;的原意是蝗虫，原作者之所以选择这个名字，估计也是听过这么一句俗语，“蝗虫过境，寸草不生”。我在网上找了张图片，大家可以感受下。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/14875962785342.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;而&lt;code&gt;Locust&lt;/code&gt;工具生成的并发请求就跟一大群蝗虫一般，对我们的被测系统发起攻击，以此检测系统在高并发压力下是否能正常运转。&lt;/p&gt;

&lt;p&gt;在&lt;a href="http://debugtalk.com/post/locustplus-talk-about-performance-test/" rel="nofollow" target="_blank" title=""&gt;《【LocustPlus 序】漫谈服务端性能测试》&lt;/a&gt;中说过，服务端性能测试工具最核心的部分是压力发生器，而压力发生器的核心要点有两个，一是真实模拟用户操作，二是模拟有效并发。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;Locust&lt;/code&gt;测试框架中，测试场景是采用纯 Python 脚本进行描述的。对于最常见的&lt;code&gt;HTTP(S)&lt;/code&gt;协议的系统，&lt;code&gt;Locust&lt;/code&gt;采用 Python 的&lt;code&gt;requests&lt;/code&gt;库作为客户端，使得脚本编写大大简化，富有表现力的同时且极具美感。而对于其它协议类型的系统，&lt;code&gt;Locust&lt;/code&gt;也提供了接口，只要我们能采用 Python 编写对应的请求客户端，就能方便地采用&lt;code&gt;Locust&lt;/code&gt;实现压力测试。从这个角度来说，&lt;code&gt;Locust&lt;/code&gt;可以用于压测任意类型的系统。&lt;/p&gt;

&lt;p&gt;在模拟有效并发方面，&lt;code&gt;Locust&lt;/code&gt;的优势在于其摒弃了进程和线程，完全基于事件驱动，使用&lt;code&gt;gevent&lt;/code&gt;提供的&lt;code&gt;非阻塞IO&lt;/code&gt;和&lt;code&gt;coroutine&lt;/code&gt;来实现网络层的并发请求，因此即使是单台压力机也能产生数千并发请求数；再加上对分布式运行的支持，理论上来说，&lt;code&gt;Locust&lt;/code&gt;能在使用较少压力机的前提下支持极高并发数的测试。&lt;/p&gt;
&lt;h2 id="脚本编写"&gt;脚本编写&lt;/h2&gt;
&lt;p&gt;编写&lt;code&gt;Locust&lt;/code&gt;脚本，是使用&lt;code&gt;Locust&lt;/code&gt;的第一步，也是最为重要的一步。&lt;/p&gt;
&lt;h3 id="简单示例"&gt;简单示例&lt;/h3&gt;
&lt;p&gt;先来看一个最简单的示例。&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;locust&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HttpLocust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebsiteTasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;123456&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;about&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/about/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebsiteUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpLocust&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;task_set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebsiteTasks&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://debugtalk.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;min_wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;max_wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个示例中，定义了针对&lt;code&gt;http://debugtalk.com&lt;/code&gt;网站的测试场景：先模拟用户登录系统，然后随机地访问首页（&lt;code&gt;/&lt;/code&gt;）和关于页面（&lt;code&gt;/about/&lt;/code&gt;），请求比例为&lt;code&gt;2:1&lt;/code&gt;；并且，在测试过程中，两次请求的间隔时间为&lt;code&gt;1~5&lt;/code&gt;秒间的随机值。&lt;/p&gt;

&lt;p&gt;那么，如上 Python 脚本是如何表达出以上测试场景的呢？&lt;/p&gt;

&lt;p&gt;从脚本中可以看出，脚本主要包含两个类，一个是&lt;code&gt;WebsiteUser&lt;/code&gt;（继承自&lt;code&gt;HttpLocust&lt;/code&gt;，而&lt;code&gt;HttpLocust&lt;/code&gt;继承自&lt;code&gt;Locust&lt;/code&gt;），另一个是&lt;code&gt;WebsiteTasks&lt;/code&gt;（继承自&lt;code&gt;TaskSet&lt;/code&gt;）。事实上，在&lt;code&gt;Locust&lt;/code&gt;的测试脚本中，所有业务测试场景都是在&lt;code&gt;Locust&lt;/code&gt;和&lt;code&gt;TaskSet&lt;/code&gt;两个类的继承子类中进行描述的。&lt;/p&gt;

&lt;p&gt;那如何理解&lt;code&gt;Locust&lt;/code&gt;和&lt;code&gt;TaskSet&lt;/code&gt;这两个类呢？&lt;/p&gt;

&lt;p&gt;简单地说，&lt;code&gt;Locust类&lt;/code&gt;就好比是一群蝗虫，而每一只蝗虫就是一个类的实例。相应的，&lt;code&gt;TaskSet类&lt;/code&gt;就好比是蝗虫的大脑，控制着蝗虫的具体行为，即实际业务场景测试对应的任务集。&lt;/p&gt;

&lt;p&gt;这个比喻可能不是很准确，接下来，我将分别对&lt;code&gt;Locust&lt;/code&gt;和&lt;code&gt;TaskSet&lt;/code&gt;两个类进行详细介绍。&lt;/p&gt;
&lt;h3 id="class HttpLocust(Locust)"&gt;class HttpLocust(Locust)&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;Locust类&lt;/code&gt;中，具有一个&lt;code&gt;client&lt;/code&gt;属性，它对应着虚拟用户作为客户端所具备的请求能力，也就是我们常说的请求方法。通常情况下，我们不会直接使用&lt;code&gt;Locust&lt;/code&gt;类，因为其&lt;code&gt;client&lt;/code&gt;属性没有绑定任何方法。因此在使用&lt;code&gt;Locust&lt;/code&gt;时，需要先继承&lt;code&gt;Locust类&lt;/code&gt;，然后在继承子类中的&lt;code&gt;client&lt;/code&gt;属性中绑定客户端的实现类。&lt;/p&gt;

&lt;p&gt;对于常见的&lt;code&gt;HTTP(S)&lt;/code&gt;协议，&lt;code&gt;Locust&lt;/code&gt;已经实现了&lt;code&gt;HttpLocust&lt;/code&gt;类，其&lt;code&gt;client&lt;/code&gt;属性绑定了&lt;code&gt;HttpSession&lt;/code&gt;类，而&lt;code&gt;HttpSession&lt;/code&gt;又继承自&lt;code&gt;requests.Session&lt;/code&gt;。因此在测试&lt;code&gt;HTTP(S)&lt;/code&gt;的&lt;code&gt;Locust脚本&lt;/code&gt;中，我们可以通过&lt;code&gt;client&lt;/code&gt;属性来使用&lt;code&gt;Python requests&lt;/code&gt;库的所有方法，包括&lt;code&gt;GET/POST/HEAD/PUT/DELETE/PATCH&lt;/code&gt;等，调用方式也与&lt;code&gt;requests&lt;/code&gt;完全一致。另外，由于&lt;code&gt;requests.Session&lt;/code&gt;的使用，因此&lt;code&gt;client&lt;/code&gt;的方法调用之间就自动具有了状态记忆的功能。常见的场景就是，在登录系统后可以维持登录状态的&lt;code&gt;Session&lt;/code&gt;，从而后续 HTTP 请求操作都能带上登录态。&lt;/p&gt;

&lt;p&gt;而对于&lt;code&gt;HTTP(S)&lt;/code&gt;以外的协议，我们同样可以使用&lt;code&gt;Locust&lt;/code&gt;进行测试，只是需要我们自行实现客户端。在客户端的具体实现上，可通过注册事件的方式，在请求成功时触发&lt;code&gt;events.request_success&lt;/code&gt;，在请求失败时触发&lt;code&gt;events.request_failure&lt;/code&gt;即可。然后创建一个继承自&lt;code&gt;Locust类&lt;/code&gt;的类，对其设置一个&lt;code&gt;client&lt;/code&gt;属性并与我们实现的客户端进行绑定。后续，我们就可以像使用&lt;code&gt;HttpLocust类&lt;/code&gt;一样，测试其它协议类型的系统。&lt;/p&gt;

&lt;p&gt;原理就是这样简单！&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;Locust类&lt;/code&gt;中，除了&lt;code&gt;client&lt;/code&gt;属性，还有几个属性需要关注下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;task_set&lt;/code&gt;: 指向一个&lt;code&gt;TaskSet&lt;/code&gt;类，&lt;code&gt;TaskSet&lt;/code&gt;类定义了用户的任务信息，该属性为必填；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_wait/min_wait&lt;/code&gt;: 每个用户执行两个任务间隔时间的上下限（毫秒），具体数值在上下限中随机取值，若不指定则默认间隔时间固定为 1 秒；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;host&lt;/code&gt;：被测系统的 host，当在终端中启动&lt;code&gt;locust&lt;/code&gt;时没有指定&lt;code&gt;--host&lt;/code&gt;参数时才会用到；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;weight&lt;/code&gt;：同时运行多个&lt;code&gt;Locust类&lt;/code&gt;时会用到，用于控制不同类型任务的执行权重。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;测试开始后，每个虚拟用户（&lt;code&gt;Locust实例&lt;/code&gt;）的运行逻辑都会遵循如下规律：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;先执行&lt;code&gt;WebsiteTasks&lt;/code&gt;中的&lt;code&gt;on_start&lt;/code&gt;（只执行一次），作为初始化；&lt;/li&gt;
&lt;li&gt;从&lt;code&gt;WebsiteTasks&lt;/code&gt;中随机挑选（如果定义了任务间的权重关系，那么就是按照权重关系随机挑选）一个任务执行；&lt;/li&gt;
&lt;li&gt;根据&lt;code&gt;Locust类&lt;/code&gt;中&lt;code&gt;min_wait&lt;/code&gt;和&lt;code&gt;max_wait&lt;/code&gt;定义的间隔时间范围（如果&lt;code&gt;TaskSet类&lt;/code&gt;中也定义了&lt;code&gt;min_wait&lt;/code&gt;或者&lt;code&gt;max_wait&lt;/code&gt;，以&lt;code&gt;TaskSet&lt;/code&gt;中的优先），在时间范围中随机取一个值，休眠等待；&lt;/li&gt;
&lt;li&gt;重复&lt;code&gt;2~3&lt;/code&gt;步骤，直至测试任务终止。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="class TaskSet"&gt;class TaskSet&lt;/h3&gt;
&lt;p&gt;再说下&lt;code&gt;TaskSet类&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;性能测试工具要模拟用户的业务操作，就需要通过脚本模拟用户的行为。在前面的比喻中说到，&lt;code&gt;TaskSet类&lt;/code&gt;好比蝗虫的大脑，控制着蝗虫的具体行为。&lt;/p&gt;

&lt;p&gt;具体地，&lt;code&gt;TaskSet类&lt;/code&gt;实现了虚拟用户所执行任务的调度算法，包括规划任务执行顺序（&lt;code&gt;schedule_task&lt;/code&gt;）、挑选下一个任务（&lt;code&gt;execute_next_task&lt;/code&gt;）、执行任务（&lt;code&gt;execute_task&lt;/code&gt;）、休眠等待（&lt;code&gt;wait&lt;/code&gt;）、中断控制（&lt;code&gt;interrupt&lt;/code&gt;）等等。在此基础上，我们就可以在&lt;code&gt;TaskSet&lt;/code&gt;子类中采用非常简洁的方式来描述虚拟用户的业务测试场景，对虚拟用户的所有行为（任务）进行组织和描述，并可以对不同任务的权重进行配置。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;TaskSet&lt;/code&gt;子类中定义任务信息时，可以采取两种方式，&lt;code&gt;@task装饰器&lt;/code&gt;和&lt;code&gt;tasks属性&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;采用&lt;code&gt;@task装饰器&lt;/code&gt;定义任务信息时，描述形式如下：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;locust&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserBehavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;采用&lt;code&gt;tasks属性&lt;/code&gt;定义任务信息时，描述形式如下：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;locust&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TaskSet&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserBehavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;test_job1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_job2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;# tasks = [(test_job1,1), (test_job1,2)] # 两种方式等价
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在如上两种定义任务信息的方式中，均设置了权重属性，即执行&lt;code&gt;test_job2&lt;/code&gt;的频率是&lt;code&gt;test_job1&lt;/code&gt;的两倍。&lt;/p&gt;

&lt;p&gt;若不指定执行任务的权重，则相当于比例为&lt;code&gt;1:1&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;locust&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserBehavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;locust&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TaskSet&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_job2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/job2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserBehavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;test_job1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_job2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# tasks = {test_job1:1, test_job2:1} # 两种方式等价
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;TaskSet&lt;/code&gt;子类中除了定义任务信息，还有一个是经常用到的，那就是&lt;code&gt;on_start&lt;/code&gt;函数。这个和&lt;code&gt;LoadRunner&lt;/code&gt;中的&lt;code&gt;vuser_init&lt;/code&gt;功能相同，在正式执行测试前执行一次，主要用于完成一些初始化的工作。例如，当测试某个搜索功能，而该搜索功能又要求必须为登录态的时候，就可以先在&lt;code&gt;on_start&lt;/code&gt;中进行登录操作；前面也提到，&lt;code&gt;HttpLocust&lt;/code&gt;使用到了&lt;code&gt;requests.Session&lt;/code&gt;，因此后续所有任务执行过程中就都具有登录态了。&lt;/p&gt;
&lt;h3 id="脚本增强"&gt;脚本增强&lt;/h3&gt;
&lt;p&gt;掌握了&lt;code&gt;HttpLocust&lt;/code&gt;和&lt;code&gt;TaskSet&lt;/code&gt;，我们就基本具备了编写测试脚本的能力。此时再回过头来看前面的案例，相信大家都能很好的理解了。&lt;/p&gt;

&lt;p&gt;然而，当面对较复杂的测试场景，可能有的同学还是会感觉无从下手；例如，很多时候脚本需要做关联或参数化处理，这些在&lt;code&gt;LoadRunner&lt;/code&gt;中集成的功能，换到&lt;code&gt;Locust&lt;/code&gt;中就不知道怎么实现了。可能也是这方面的原因，造成很多测试人员都感觉难以将 Locust 应用到实际的性能测试工作当中。&lt;/p&gt;

&lt;p&gt;其实这也跟&lt;code&gt;Locust&lt;/code&gt;的目标定位有关，&lt;code&gt;Locust&lt;/code&gt;的定位就是&lt;code&gt;small and very hackable&lt;/code&gt;。但是小巧并不意味着功能弱，我们完全可以通过 Python 脚本本身来实现各种各样的功能，如果大家有疑问，我们不妨逐项分解来看。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;LoadRunner&lt;/code&gt;这款功能全面应用广泛的商业性能测试工具中，脚本增强无非就涉及到四个方面：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;关联&lt;/li&gt;
&lt;li&gt;参数化&lt;/li&gt;
&lt;li&gt;检查点&lt;/li&gt;
&lt;li&gt;集合点&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;先说关联这一项。在某些请求中，需要携带之前从 Server 端返回的参数，因此在构造请求时需要先从之前请求的 Response 中提取出所需的参数，常见场景就是&lt;code&gt;session_id&lt;/code&gt;。针对这种情况，&lt;code&gt;LoadRunner&lt;/code&gt;虽然可能通过录制脚本进行自动关联，但是效果并不理想，在实际测试过程中也基本都是靠测试人员手动的来进行关联处理。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;LoadRunner&lt;/code&gt;中手动进行关联处理时，主要是通过使用注册型函数，例如&lt;code&gt;web_reg_save_param&lt;/code&gt;，对前一个请求的响应结果进行解析，根据左右边界或其它特征定位到参数值并将其保存到参数变量，然后在后续请求中使用该参数。采用同样的思想，我们在&lt;code&gt;Locust&lt;/code&gt;脚本中也完全可以实现同样的功能，毕竟只是 Python 脚本，通过官方库函数&lt;code&gt;re.search&lt;/code&gt;就能实现所有需求。甚至针对 html 页面，我们也可以采用&lt;code&gt;lxml&lt;/code&gt;库，通过&lt;code&gt;etree.HTML(html).xpath&lt;/code&gt;来更优雅地实现元素定位。&lt;/p&gt;

&lt;p&gt;然后再来看参数化这一项。这一项极其普遍，主要是用在测试数据方面。但通过归纳，发现其实也可以概括为三种类型。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;循环取数据，数据可重复使用：e.g. 模拟 3 用户并发请求网页，总共有 100 个 URL 地址，每个虚拟用户都会依次循环加载这 100 个 URL 地址；&lt;/li&gt;
&lt;li&gt;保证并发测试数据唯一性，不循环取数据：e.g. 模拟 3 用户并发注册账号，总共有 90 个账号，要求注册账号不重复，注册完毕后结束测试；&lt;/li&gt;
&lt;li&gt;保证并发测试数据唯一性，循环取数据：模拟 3 用户并发登录账号，总共有 90 个账号，要求并发登录账号不相同，但数据可循环使用。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;通过以上归纳，可以确信地说，以上三种类型基本上可以覆盖我们日常性能测试工作中的所有参数化场景。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;LoadRunner&lt;/code&gt;中是有一个集成的参数化模块，可以直接配置参数化策略。那在&lt;code&gt;Locust&lt;/code&gt;要怎样实现该需求呢？&lt;/p&gt;

&lt;p&gt;答案依旧很简单，使用 Python 的&lt;code&gt;list&lt;/code&gt;和&lt;code&gt;queue&lt;/code&gt;数据结构即可！具体做法是，在&lt;code&gt;WebsiteUser&lt;/code&gt;定义一个数据集，然后所有虚拟用户在&lt;code&gt;WebsiteTasks&lt;/code&gt;中就可以共享该数据集了。如果不要求数据唯一性，数据集选择&lt;code&gt;list&lt;/code&gt;数据结构，从头到尾循环遍历即可；如果要求数据唯一性，数据集选择&lt;code&gt;queue&lt;/code&gt;数据结构，取数据时进行&lt;code&gt;queue.get()&lt;/code&gt;操作即可，并且这也不会循环取数据；至于涉及到需要循环取数据的情况，那也简单，每次取完数据后再将数据插入到队尾即可，&lt;code&gt;queue.put_nowait(data)&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;最后再说下检查点。该功能在&lt;code&gt;LoadRunner&lt;/code&gt;中通常是使用&lt;code&gt;web_reg_find&lt;/code&gt;这类注册函数进行检查的。在&lt;code&gt;Locust&lt;/code&gt;脚本中，处理就更方便了，只需要对响应的内容关键字进行&lt;code&gt;assert xxx in response&lt;/code&gt;操作即可。&lt;/p&gt;

&lt;p&gt;针对如上各种脚本增强的场景，我也通过代码示例分别进行了演示。但考虑到文章中插入太多代码会影响到阅读，因此将代码示例部分剥离了出来，如有需要请点击查看&lt;a href="http://debugtalk.com/post/head-first-locust-advanced-script/" rel="nofollow" target="_blank" title=""&gt;《深入浅出开源性能测试工具 Locust（脚本增强）》&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="硬广"&gt;硬广&lt;/h2&gt;
&lt;p&gt;欢迎关注我的个人博客和微信公众号。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;个人博客：&lt;a href="http://debugtalk.com" rel="nofollow" target="_blank"&gt;http://debugtalk.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;微信公众号：&lt;a href="http://debugtalk.com/images/wechat_qrcode.png" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DebugTalk&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>debugtalk</author>
      <pubDate>Wed, 22 Feb 2017 21:28:25 +0800</pubDate>
      <link>https://ruby-china.org/topics/32363</link>
      <guid>https://ruby-china.org/topics/32363</guid>
    </item>
    <item>
      <title>[LocustPlus 序] 漫谈服务端性能测试</title>
      <description>&lt;p&gt;最近因为工作原因，我又拾起了老本行，开始做 Web 性能测试。之前虽然做过三四年的性能测试，但是在博客和开源项目方面都没有什么输出，一直是一个很大的遗憾。因此，近期打算围绕服务端性能测试的话题，将自己在这方面的经历进行整理。并且，最近使用的性能测试工具 Locust 感觉挺不错的，只是其功能比较单薄，特别是在性能指标监控和测试报告图表方面比较缺失，因此也打算在 Locust 的基础上做二次开发，打造一款自己用得顺手的性能测试工具，暂且将其命名为 LocustPlus 吧。&lt;/p&gt;

&lt;p&gt;&lt;a href="http://debugtalk.com/post/locustplus-talk-about-performance-test/" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/locustplus-talk-about-performance-test/&lt;/a&gt;&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Wed, 02 Nov 2016 20:48:10 +0800</pubDate>
      <link>https://ruby-china.org/topics/31510</link>
      <guid>https://ruby-china.org/topics/31510</guid>
    </item>
    <item>
      <title>打造心目中理想的自动化测试框架 (AppiumBooster)</title>
      <description>&lt;p&gt;做过自动化测试的人应该都会有这样一种体会，要写个自动化 demo 测试用例很容易，但是要真正将自动化测试落地，对成百上千的自动化测试用例实现较好的可复用性和可维护性就很难了。&lt;/p&gt;

&lt;p&gt;基于这一痛点，我开发了&lt;a href="https://github.com/debugtalk/AppiumBooster" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;AppiumBooster&lt;/code&gt;&lt;/a&gt;框架。顾名思义，&lt;code&gt;AppiumBooster&lt;/code&gt;基于&lt;code&gt;Appium&lt;/code&gt;实现，但更简单和易于使用；测试人员不用接触任何代码，就可以直接采用简洁优雅的方式来编写和维护自动化测试用例。&lt;/p&gt;

&lt;p&gt;原型开发完毕后，我将其应用在当前所在团队的项目上，并在使用的过程中，按照自己心目中理想的自动化测试框架的模样对其进行迭代优化，最终打磨成了一个自己还算用得顺手的自动化测试框架。&lt;/p&gt;

&lt;p&gt;本文便是对&lt;code&gt;AppiumBooster&lt;/code&gt;的核心特性及其设计思想进行介绍。在内容组织上，本文的各个部分相对独立，大家可直接选择自己感兴趣的部分进行阅读。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;原文链接：&lt;a href="http://debugtalk.com/post/build-ideal-app-automation-test-framework/" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/build-ideal-app-automation-test-framework/&lt;/a&gt;
项目源码：&lt;a href="https://github.com/debugtalk/AppiumBooster" rel="nofollow" target="_blank"&gt;https://github.com/debugtalk/AppiumBooster&lt;/a&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/wechat_qrcode.png" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Wed, 07 Sep 2016 15:32:01 +0800</pubDate>
      <link>https://ruby-china.org/topics/31005</link>
      <guid>https://ruby-china.org/topics/31005</guid>
    </item>
    <item>
      <title>Jenkins 的输出日志也可以变得色色的</title>
      <description>&lt;p&gt;在《&lt;a href="http://debugtalk.com/post/Jenkins-CI-Automation-Test/" rel="nofollow" target="_blank" title=""&gt;使用 Jenkins 实现持续集成构建检查&lt;/a&gt;》一文中，写到了这么一段话：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;在这里，我们还可以通过--disable_output_color 开关将输出日志的颜色关闭。之所以实现这么一个功能，是因为在 Jenkins 中本来也无法显示颜色，但是如果还将 Terminal 中有颜色的日志内容输出到 Jenkins 中，就会出现一些额外的字符，比较影响日志的美观。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;非常感谢热心的读者，及时地为我纠正了这一点。事实上，当前在 Jenkins 中，是可以通过安装插件来实现在输出日志中显示颜色的。&lt;/p&gt;

&lt;p&gt;这个插件就是&lt;code&gt;AnsiColor&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="安装 &amp;amp;&amp;amp; 配置"&gt;安装 &amp;amp;&amp;amp; 配置&lt;/h2&gt;
&lt;p&gt;安装的方式很简单，【Manage Jenkins】-&amp;gt;【Manage Plugins】，搜索&lt;code&gt;AnsiColor&lt;/code&gt;进行安装即可。&lt;/p&gt;

&lt;p&gt;安装完成后，在 Jenkins Project 的&lt;code&gt;Configure&lt;/code&gt;页面中，&lt;code&gt;Build Environment&lt;/code&gt;栏目下会多出&lt;code&gt;Color ANSI Console Output&lt;/code&gt;配置项，勾选后即可开启颜色输出配置。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/Jenkins_Color_ANSI_Console_Output.jpg" title="" alt="Jenkins Color ANSI Console Output"&gt;&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;ANSI color map&lt;/code&gt;的列表选择框中，存在多个选项，默认情况下，选择&lt;code&gt;xterm&lt;/code&gt;即可。&lt;/p&gt;

&lt;p&gt;保存配置后，再次执行构建时，就可以在&lt;code&gt;Console&lt;/code&gt;中看到颜色输出了。&lt;/p&gt;
&lt;h2 id="效果图"&gt;效果图&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;xctool&lt;/code&gt;命令编译 iOS 应用时，在 Jenkins 的&lt;code&gt;Console output&lt;/code&gt;中会看到和&lt;code&gt;Terminal&lt;/code&gt;中一样的颜色效果。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/Jenkins_Console_Output_Colored.jpg" title="" alt="Jenkins Console Output Colored"&gt;&lt;/p&gt;
&lt;h2 id="补充说明"&gt;补充说明&lt;/h2&gt;
&lt;p&gt;需要说明的是，在输出日志中显示颜色，依赖于输出的日志本身。也就是说，如果输出日志时并没有&lt;code&gt;ANSI escape sequences&lt;/code&gt;，那么安装该插件后也没有任何作用，并不会凭空给日志加上颜色。&lt;/p&gt;

&lt;p&gt;例如，如果采用&lt;code&gt;xcodebuild&lt;/code&gt;命令编译 iOS 应用，那么输出日志就不会显示颜色。&lt;/p&gt;

&lt;p&gt;说到这里，再简单介绍下&lt;code&gt;ANSI escape sequences&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="ANSI escape sequences"&gt;ANSI escape sequences&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/ANSI_escape_code" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;ANSI escape sequences&lt;/code&gt;&lt;/a&gt;，也叫&lt;code&gt;ANSI escape codes&lt;/code&gt;，主要是用于对 Terminal 中的文本字符进行颜色的控制，包括字符背景颜色和字符颜色。&lt;/p&gt;

&lt;p&gt;使用方式如下：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;33[字符背景颜色;字符颜色m{String}33[0m&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;其中，&lt;code&gt;33[字符背景颜色;字符颜色m&lt;/code&gt;是开始标识，&lt;code&gt;33[0m&lt;/code&gt;是结束标识，&lt;code&gt;{String}&lt;/code&gt;是原始文本内容。通过这种形式，就可以对输出的文本颜色进行控制。&lt;/p&gt;

&lt;p&gt;具体地，字符颜色和字符背景颜色的编码如下：&lt;/p&gt;

&lt;p&gt;字符颜色（foreground color）：30~37&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;30:黑&lt;/li&gt;
&lt;li&gt;31:红&lt;/li&gt;
&lt;li&gt;32:绿&lt;/li&gt;
&lt;li&gt;33:黄&lt;/li&gt;
&lt;li&gt;34:蓝色&lt;/li&gt;
&lt;li&gt;35:紫色&lt;/li&gt;
&lt;li&gt;36:深绿&lt;/li&gt;
&lt;li&gt;37:白色&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;字符背景颜色（background color）：40~47&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;40:黑&lt;/li&gt;
&lt;li&gt;41:深红&lt;/li&gt;
&lt;li&gt;42:绿&lt;/li&gt;
&lt;li&gt;43:黄色&lt;/li&gt;
&lt;li&gt;44:蓝色&lt;/li&gt;
&lt;li&gt;45:紫色&lt;/li&gt;
&lt;li&gt;46:深绿&lt;/li&gt;
&lt;li&gt;47:白色&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;需要说明的是，字符背景颜色和字符颜色并非必须同时设置，也可以只设置一项。&lt;/p&gt;
&lt;h2 id="代码示例"&gt;代码示例&lt;/h2&gt;
&lt;p&gt;掌握了以上概念后，我们就可以通过对打印日志的代码进行一点调整，然后就可以让输出的日志更加美观了。&lt;/p&gt;

&lt;p&gt;以 Ruby 为例，在&lt;code&gt;String&lt;/code&gt;基础类中添加一些展示颜色的方法。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;
  &lt;span class="c1"&gt;# colorization&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;colorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\e&lt;/span&gt;&lt;span class="s2"&gt;[&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;color_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;m&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\e&lt;/span&gt;&lt;span class="s2"&gt;[0m"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;red&lt;/span&gt;
    &lt;span class="n"&gt;colorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;green&lt;/span&gt;
    &lt;span class="n"&gt;colorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;yellow&lt;/span&gt;
    &lt;span class="n"&gt;colorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，我们在打印日志时就可以通过如下方式来控制日志的颜色了。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 步骤执行正常，输出为绿色&lt;/span&gt;
&lt;span class="n"&gt;step_action_desc&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;"    ...    ✓"&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;step_action_desc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;green&lt;/span&gt;

&lt;span class="c1"&gt;# 步骤执行异常，输出为红色&lt;/span&gt;
&lt;span class="n"&gt;step_action_desc&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;"    ...    ✖"&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;step_action_desc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;red&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;展示效果如下图所示。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/Terminal_Output_Colored.jpg" title="" alt="Terminal Output Colored"&gt;&lt;/p&gt;

&lt;p&gt;是不是好看多了？&lt;/p&gt;

&lt;p&gt;原文链接：&lt;a href="http://debugtalk.com/post/make-Jenkins-Console-Output-Colorful/" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/make-Jenkins-Console-Output-Colorful/&lt;/a&gt;&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Tue, 16 Aug 2016 19:26:47 +0800</pubDate>
      <link>https://ruby-china.org/topics/30827</link>
      <guid>https://ruby-china.org/topics/30827</guid>
    </item>
    <item>
      <title>使用 Jenkins 实现持续集成构建检查</title>
      <description>&lt;p&gt;通过&lt;a href="http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins/" rel="nofollow" target="_blank" title=""&gt;《使用 Jenkins 搭建 iOS/Android 持续集成打包平台》&lt;/a&gt;和&lt;a href="http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins-details/" rel="nofollow" target="_blank" title=""&gt;《关于持续集成打包平台的 Jenkins 配置和构建脚本实现细节》&lt;/a&gt;两篇文章，我们已经在原理概念和实践操作两个层面掌握了如何搭建一个完整的持续集成打包平台。&lt;/p&gt;

&lt;p&gt;不过，在实际使用过程中，发现有时候还会存在一个问题。研发同学提交新的代码后，Jenkins 端可以成功执行构建，并生成安装包；然而在将安装包安装至移动设备时，却发现有时候会出现无法成功安装，或者安装后出现启动闪退的情况。&lt;/p&gt;

&lt;p&gt;为了及时发现该类问题，我们还需要对每次构建生成的安装包进行检查。本文便是对构建检查涉及到的方法进行介绍。&lt;/p&gt;
&lt;h2 id="构建生成.app"&gt;构建生成&lt;code&gt;.app&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;为了降低问题的复杂度，我们可以选择在模拟器中运行构建生成的安装包。之前在&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-Appium-inspector-iOS-simulator/" rel="nofollow" target="_blank" title=""&gt;《从 0 到 1 搭建移动 App 功能自动化测试平台（1）：模拟器中运行 iOS 应用》&lt;/a&gt;也讲解过，要在模拟器中运行 iOS 应用，需要在 Xcode 中编译时选择模拟器类型，并且编译生成的文件后缀为&lt;code&gt;.app&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;对应的，构建生成&lt;code&gt;.app&lt;/code&gt;的命令如下：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# build .app file from source code&lt;/span&gt;
xcodebuild &lt;span class="se"&gt;\ &lt;/span&gt;   &lt;span class="c"&gt;# xctool&lt;/span&gt;
  &lt;span class="nt"&gt;-workspace&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKSPACE_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-scheme&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SCHEME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-configuration&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIGURATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-sdk&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SDK&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-derivedDataPath&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OUTPUT_FOLDER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;xcodebuild/xctool参数说明&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-workspace&lt;/code&gt;：需要打包的 workspace，后面接的文件一定要是&lt;code&gt;.xcworkspace&lt;/code&gt;结尾的；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-scheme&lt;/code&gt;：需要打包的 Scheme，一般与&lt;code&gt;$project_name&lt;/code&gt;相同；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-sdk&lt;/code&gt;：区分 iphone device 和 Simulator，可通过&lt;code&gt;xcodebuild -showsdks&lt;/code&gt;获取，例如&lt;code&gt;iphonesimulator9.3&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-configuration&lt;/code&gt;：需要打包的配置文件，我们一般在项目中添加多个配置，适合不同的环境，Release/Debug；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-derivedDataPath&lt;/code&gt;：指定编译结果文件的存储路径；例如，指定&lt;code&gt;-derivedDataPath build_outputs&lt;/code&gt;时，将在项目根目录下创建一个&lt;code&gt;build_outputs&lt;/code&gt;文件夹，生成的&lt;code&gt;.app&lt;/code&gt;文件将位于&lt;code&gt;build_outputs/Build/Products/${CONFIGURATION}-iphoneos&lt;/code&gt;中。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;同样地，这里也可以使用&lt;code&gt;xctool&lt;/code&gt;代替&lt;code&gt;xcodebuild&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这里使用到的命令和参数基本上和&lt;a href="http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins-details/" rel="nofollow" target="_blank" title=""&gt;《关于持续集成打包平台的 Jenkins 配置和构建脚本实现细节》&lt;/a&gt;一文中的大致相同，唯一需要特别注意的是&lt;code&gt;-sdk&lt;/code&gt;参数。因为是要在模拟器中运行，因此&lt;code&gt;-sdk&lt;/code&gt;参数要设置为&lt;code&gt;iphonesimulator&lt;/code&gt;，而非&lt;code&gt;iphoneos&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;命令成功执行后，就会在指定的&lt;code&gt;${OUTPUT_FOLDER}&lt;/code&gt;目录中生成&lt;code&gt;${SCHEME}.app&lt;/code&gt;文件，这也就是我们构建生成的产物。另外，熟悉&lt;code&gt;iOS&lt;/code&gt;的同学都知道，&lt;code&gt;.app&lt;/code&gt;文件其实是一个文件夹，为了实现更好的存储，我们也可以额外地再做一步操作，将&lt;code&gt;.app&lt;/code&gt;文件夹压缩转换为&lt;code&gt;.zip&lt;/code&gt;格式，而且 Appium 也是支持读取&lt;code&gt;.zip&lt;/code&gt;格式的安装包的。&lt;/p&gt;

&lt;p&gt;至此，适用于 iOS 模拟器运行的构建产物已准备就绪。这里涉及到的脚本实现已更新至&lt;a href="https://github.com/debugtalk/JenkinsTemplateForApp" rel="nofollow" target="_blank" title=""&gt;【debugtalk/JenkinsTemplateForApp】&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="实现构建检查"&gt;实现构建检查&lt;/h2&gt;
&lt;p&gt;那要怎样对构建生成的产物进行检查呢？&lt;/p&gt;

&lt;p&gt;最简单的方式，就是在 iOS 模拟器中运行构建生成的&lt;code&gt;.app&lt;/code&gt;，并执行一组基本的自动化测试用例。在执行过程中，如果出现无法成功安装，或者安装成功后启动出现闪退，或者自动化测试用例执行失败等异常情况，则说明我们最新提交的代码存在问题，需要通知研发同学及时进行修复。&lt;/p&gt;

&lt;p&gt;而这些实现方式，其实我在&lt;a href="http://debugtalk.com/tags/F0T1/" rel="nofollow" target="_blank" title=""&gt;《从 0 到 1 搭建移动 App 功能自动化测试平台》&lt;/a&gt;系列文章中都已经进行了详细讲解，并形成了一套较为成熟的自动化测试框架，&lt;a href="https://github.com/debugtalk/AppiumBooster" rel="nofollow" target="_blank" title=""&gt;【debugtalk/AppiumBooster】&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;在此基础上，我们无需再做其它工作，只需要按照&lt;code&gt;debugtalk/AppiumBooster&lt;/code&gt;的要求在表格中编写一组基本的自动化测试用例，即可采用如下方式执行构建检查。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;AppiumBooster_Folder&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;ruby run.rb &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OUTPUT_FOLDER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SCHEME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.app.zip"&lt;/span&gt; &lt;span class="nt"&gt;--disable_output_color&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; test_result.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在如上命令中，通过&lt;code&gt;-p&lt;/code&gt;参数指定构建生成的安装包，然后就可以在 iOS 模拟器中运行事先编写好的自动化测试用例，从而实现构建检查。&lt;/p&gt;

&lt;p&gt;在这里，我们还可以通过&lt;code&gt;--disable_output_color&lt;/code&gt;开关将输出日志的颜色关闭。之所以实现这么一个功能，是因为在 Jenkins 中本来也无法显示颜色，但是如果还将 Terminal 中有颜色的日志内容输出到 Jenkins 中，就会出现一些额外的字符，比较影响日志的美观。&lt;/p&gt;

&lt;p&gt;现在，我们已经分别实现了代码构建和构建检查这两个核心的操作环节，而要执行最终的持续集成，我们还需要做最后一项工作，即在 Jenkins 中将这两个环节串联起来。&lt;/p&gt;
&lt;h2 id="Jenkins配置"&gt;Jenkins 配置&lt;/h2&gt;
&lt;p&gt;关于 Jenkins 的相关基础概念、实施流程和配置细节，我在之前的&lt;a href="http://debugtalk.com/tags/Jenkins/" rel="nofollow" target="_blank" title=""&gt;文章&lt;/a&gt;中已经讲解得非常详细了。在此我就只进行一点补充。&lt;/p&gt;

&lt;p&gt;要实现在构建完成后再运行一些额外的脚本，例如我们的构建检查命令，需要使用到 Jenkins 的一个插件，&lt;code&gt;Post-Build Script Plug-in&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;安装完该插件后，在 Jenkins 配置界面的&lt;code&gt;Post-build Actions&lt;/code&gt;栏目中，&lt;code&gt;Add post-build action&lt;/code&gt;选项列表中就会多出&lt;code&gt;Execute a set of scripts&lt;/code&gt;选项。选择该项后，会出现如下配置界面。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/Jenkins_Post_build_Actions_Execute_shell_menu.jpg" title="" alt="Jenkins Post_build_Actions Execute_shell menu"&gt;&lt;/p&gt;

&lt;p&gt;选择&lt;code&gt;Execute shell&lt;/code&gt;后，会出现一个文本框，然后我们就可以将构建检查的命令填写到里面。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/Jenkins_Post_build_Actions_Execute_shell.jpg" title="" alt="Jenkins Post_build_Actions Execute_shell"&gt;&lt;/p&gt;

&lt;p&gt;在这里我们用到了&lt;code&gt;${AppiumBooster_Folder}&lt;/code&gt;参数，该参数也需要通过&lt;code&gt;String Parameter&lt;/code&gt;来进行定义，用于指定&lt;code&gt;AppiumBooster&lt;/code&gt;项目的路径。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/Jenkins_String_Parameter.jpg" title="" alt="Jenkins String Parameter"&gt;&lt;/p&gt;

&lt;p&gt;最后，为了便于将执行自动化测试用例的日志和执行构建的日志分开，我们将执行自动化测试用例的日志写入到了&lt;code&gt;test_result.log&lt;/code&gt;文件中。然后，在&lt;code&gt;Archives build artifacts&lt;/code&gt;中就可以通过&lt;code&gt;${AppiumBooster_Folder}/test_result.log&lt;/code&gt;将执行构建检查的日志收集起来，并展示到每次构建的页面中。&lt;/p&gt;

&lt;p&gt;延续一贯的&lt;code&gt;开箱即用&lt;/code&gt;原则，我将使用 Jenkins 实现持续集成构建检查涉及到的 Jenkins 配置也做成了一套模板，并更新到&lt;a href="https://github.com/debugtalk/JenkinsTemplateForApp" rel="nofollow" target="_blank" title=""&gt;【debugtalk/JenkinsTemplateForApp】&lt;/a&gt;中了，供大家参考。&lt;/p&gt;
&lt;h2 id="写在最后"&gt;写在最后&lt;/h2&gt;
&lt;p&gt;至此，通过&lt;a href="http://debugtalk.com/tags/Jenkins/" rel="nofollow" target="_blank" title=""&gt;本系列的几篇文章&lt;/a&gt;，关于如何使用 Jenkins 实现移动 APP 持续集成的相关内容应该都已经覆盖得差不多了。&lt;/p&gt;

&lt;p&gt;不过，由于我个人的近期工作主要集中在 iOS 部分，因此在讲解的过程中都是以 iOS 为主。后续在将工作重心移到 Android 部分后，我会再在&lt;code&gt;DebugTalk&lt;/code&gt;的这几篇文章中更新 Android 部分的内容。&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Sun, 14 Aug 2016 19:58:05 +0800</pubDate>
      <link>https://ruby-china.org/topics/30816</link>
      <guid>https://ruby-china.org/topics/30816</guid>
    </item>
    <item>
      <title>从 0 到 1 搭建移动 App 功能自动化测试平台 (4)：自动化测试代码⎡工程化⎦</title>
      <description>&lt;p&gt;在本系列的&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-write-iOS-testcase-scripts/" rel="nofollow" target="_blank" title=""&gt;上一篇文章&lt;/a&gt;中，我通过系统登录这一典型功能点，演示了编写自动化测试脚本的整个流程，并对测试脚本进行了初步优化。&lt;/p&gt;

&lt;p&gt;在本文中，我将重点介绍如何对自动化测试脚本实现⎡工程化⎦的组织和管理。&lt;/p&gt;
&lt;h2 id="测试脚本⎡工程化⎦"&gt;测试脚本⎡工程化⎦&lt;/h2&gt;
&lt;p&gt;首先说下什么是测试脚本的工程化。&lt;/p&gt;

&lt;p&gt;通过之前的工作，我们已经可以让单个自动化测试用例正常运行起来了。然而，这还只算是一个&lt;a href="https://github.com/debugtalk/AppiumBooster/tree/0.RawScript/" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;demo&lt;/code&gt;&lt;/a&gt;，一切才刚刚开始。&lt;/p&gt;

&lt;p&gt;试想，一个项目的自动化测试用例少则数百，多则成千上万。如何将这些自动化测试用例组织起来？如何实现更好的可重用机制？如何实现更好的可拓展机制？这些都还是我们当前的 demo 所不具备的，也是我们需要通过“工程化”手段进行改造的原因。&lt;/p&gt;
&lt;h2 id="引入Minitest/RSpec"&gt;引入 Minitest/RSpec&lt;/h2&gt;
&lt;p&gt;在 Ruby 中，说到测试首先就会想到 Minitest 或 RSpec，这是 Ruby 中用的最多的两个测试框架。通过这些框架，我们可以很好地实现对 Ruby 测试用例的管理。&lt;/p&gt;

&lt;p&gt;同样地，由于我们的自动化测试脚本是采用 Ruby 编写的，因此我们也可以使用 Minitest/RSpec 来管理我们的自动化测试用例。&lt;/p&gt;

&lt;p&gt;基于该想法，我们采用 RSpec 对之前的系统登录测试用例进行工程结构初始化。对于熟悉 Ruby 编程，或者有一定代码基础的同学而言，很自然地，可以将测试用例框架初始化为如下结构。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;├── Gemfile
├── android
│   └── appium.txt
├── common
│   ├── requires.rb
│   └── spec_helper.rb
└── ios
    ├── appium.txt
    └── spec
        └── login_spec.rb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;Gemfile&lt;/code&gt;中，指定了项目依赖的库。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# filename: Gemfile&lt;/span&gt;
&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="s1"&gt;'https://gems.ruby-china.org'&lt;/span&gt;

&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rspec'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'appium_lib'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'appium_console'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;common/spec_helper.rb&lt;/code&gt;中，定义了模拟器和 RSpec 初始化相关的代码。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# filename: common/spec_helper.rb&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup_driver&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vg"&gt;$driver&lt;/span&gt;
  &lt;span class="n"&gt;appium_txt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ios'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'appium.txt'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;caps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Appium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_appium_txt&lt;/span&gt; &lt;span class="ss"&gt;file: &lt;/span&gt;&lt;span class="n"&gt;appium_txt&lt;/span&gt;
  &lt;span class="no"&gt;Appium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="n"&gt;caps&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;promote_methods&lt;/span&gt;
  &lt;span class="no"&gt;Appium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;promote_appium_methods&lt;/span&gt; &lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Core&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ExampleGroup&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;setup_driver&lt;/span&gt;
&lt;span class="n"&gt;promote_methods&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;

  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="vg"&gt;$driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_driver&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;alert_accept&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;driver_quit&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;common/requires.rb&lt;/code&gt;中，实现了对相关库文件的引用。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# filename: common/requires.rb&lt;/span&gt;

&lt;span class="c1"&gt;# load lib&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'rspec'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'appium_lib'&lt;/span&gt;

&lt;span class="c1"&gt;# setup rspec&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'spec_helper'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;ios/appium.txt&lt;/code&gt;中，对 iOS 模拟器信息和测试包路径进行了配置。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;platformName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ios"&lt;/span&gt;
&lt;span class="n"&gt;deviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"iPhone 6s"&lt;/span&gt;
&lt;span class="n"&gt;platformVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"9.3"&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/Users/Leo/MyProjects/AppiumBooster/ios/app/test.app"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;ios/spec/&lt;/code&gt;目录中，则是测试用例的内容。例如，&lt;code&gt;ios/spec/login_spec.rb&lt;/code&gt;对应的就是系统登录的测试用例。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# filename: ios/spec/login_spec.rb&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'../../common/requires'&lt;/span&gt;

&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'Login'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;

  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'with valid account'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'btnMenuMyAccount'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'uiviewMyAccount'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tablecellMyAccountLogin'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'uiviewLogIn'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'txtfieldEmailAddress'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'leo.lee@dji.com'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sectxtfieldPassword'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'123321'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'btnLogin'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'tablecellMyMessage'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过以上代码结构初始化，我们的测试用例框架的雏形就形成了。接下来，在 Terminal 中切换到项目根目录，然后通过&lt;code&gt;rspec ios&lt;/code&gt;命令就可以执行 ios 目录中的测试用例了。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ rspec ios
&lt;span class="nb"&gt;.&lt;/span&gt;

Finished &lt;span class="k"&gt;in &lt;/span&gt;2 minutes 7.2 seconds &lt;span class="o"&gt;(&lt;/span&gt;files took 1.76 seconds to load&lt;span class="o"&gt;)&lt;/span&gt;
1 example, 0 failures
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的代码请参考&lt;code&gt;debugtalk/AppiumBooster&lt;/code&gt;的&lt;a href="https://github.com/debugtalk/AppiumBooster/tree/1.FirstTest" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;1.FirstTest&lt;/code&gt;分支&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="添加第二条测试用例"&gt;添加第二条测试用例&lt;/h2&gt;
&lt;p&gt;现在，我们尝试往当前的测试框架中添加第二条测试用例。&lt;/p&gt;

&lt;p&gt;例如，第二条测试用例要实现启动后从当前地区切换至中国。那么，就可以新增&lt;code&gt;ios/spec/change_country_spec.rb&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# filename: ios/spec/change_country_spec.rb&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'../../common/requires'&lt;/span&gt;

&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'Change country'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;

  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'from Hong Kong to China'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'btnMenuMyAccount'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'uiviewMyAccount'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tablecellMyAccountSystemSettings'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'txtCountryDistrict'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'txtCountryDistrict'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'uiviewSelectCountry'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tablecellSelectCN'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'btnArrowLeft'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'uiviewMyAccount'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的代码请参考&lt;code&gt;debugtalk/AppiumBooster&lt;/code&gt;的&lt;a href="https://github.com/debugtalk/AppiumBooster/tree/2.SecondTest" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;2.SecondTest&lt;/code&gt;分支&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;现在我们凝视已经添加的&lt;a href="https://github.com/debugtalk/AppiumBooster/tree/2.SecondTest/ios/spec" rel="nofollow" target="_blank" title=""&gt;两个测试用例&lt;/a&gt;，有发现什么问题么？&lt;/p&gt;

&lt;p&gt;是的，重复代码太多。在每一步操作中，都要用&lt;code&gt;id&lt;/code&gt;来定位控件，还要用&lt;code&gt;wait&lt;/code&gt;来实现等待机制。&lt;/p&gt;

&lt;p&gt;除此之外，当前代码最大的问题就是测试用例与控件映射杂糅在一起。造成的后果就是，不管是控件映射发生变动，还是测试用例需要修改，都要来修改这一份代码，维护难度较大。&lt;/p&gt;
&lt;h2 id="重构：测试用例与控件映射分离"&gt;重构：测试用例与控件映射分离&lt;/h2&gt;
&lt;p&gt;基于以上问题，我们首要的改造任务就是将测试用例与控件映射进行分离。&lt;/p&gt;

&lt;p&gt;考虑到常用的控件操作方法就只有几个（&lt;code&gt;click&lt;/code&gt;，&lt;code&gt;type&lt;/code&gt;），因此我们可以将控件操作方法单独封装为一个模块，作为公共模块。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Actions&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;click&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vi"&gt;@found_cell.click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vi"&gt;@found_cell.type&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，将 APP 中每一个页面封装为一个模块（&lt;code&gt;module&lt;/code&gt;），将页面中的控件映射为模块的静态方法（&lt;code&gt;method&lt;/code&gt;），并通过&lt;code&gt;include&lt;/code&gt;机制引入方法模块。&lt;/p&gt;

&lt;p&gt;例如，登录页面就可以封装为如下代码。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Pages&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Login&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;

      &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Actions&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;field_Email_Address&lt;/span&gt;
        &lt;span class="vi"&gt;@found_cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'txtfieldEmailAddress'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;field_Password&lt;/span&gt;
        &lt;span class="vi"&gt;@found_cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'sectxtfieldPassword'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;button_Login&lt;/span&gt;
        &lt;span class="vi"&gt;@found_cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'btnLogin'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Kernel&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;
    &lt;span class="no"&gt;Pages&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Login&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里还用到了一点 Ruby 元编程技巧，就是将页面模块封装为一个方法，并加入到&lt;code&gt;Kernel&lt;/code&gt;模块下。这样做的好处就是，我们可以在项目的任意地方直接通过&lt;code&gt;login.button_Login.click&lt;/code&gt;这样的形式来对控件进行操作了。&lt;/p&gt;

&lt;p&gt;完成以上改造后，系统登录测试用例就可以采用如下形式进行编写了。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'Login'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;

  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'with valid account'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# switch to My Account page&lt;/span&gt;
    &lt;span class="n"&gt;my_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button_My_Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
    &lt;span class="n"&gt;inner_screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_control&lt;/span&gt; &lt;span class="s1"&gt;'uiviewMyAccount'&lt;/span&gt;

    &lt;span class="c1"&gt;# enter login page&lt;/span&gt;
    &lt;span class="n"&gt;my_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button_Login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
    &lt;span class="n"&gt;inner_screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_control&lt;/span&gt; &lt;span class="s1"&gt;'uiviewLogIn'&lt;/span&gt;

    &lt;span class="c1"&gt;# login&lt;/span&gt;
    &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field_Email_Address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'leo.lee@dji.com'&lt;/span&gt;
    &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field_Password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'123321'&lt;/span&gt;
    &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button_Login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
    &lt;span class="n"&gt;inner_screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_control&lt;/span&gt; &lt;span class="s1"&gt;'tablecellMyMessage'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的代码请参考&lt;code&gt;debugtalk/AppiumBooster&lt;/code&gt;的&lt;a href="https://github.com/debugtalk/AppiumBooster/tree/3.RefactorV1" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;3.RefactorV1&lt;/code&gt;分支&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="To be continued ..."&gt;To be continued ...&lt;/h2&gt;
&lt;p&gt;经过这一轮重构，我们的测试用例与控件映射已经实现了分离，测试用例的可重用性与可扩展性也得到了极大的提升。&lt;/p&gt;

&lt;p&gt;然而，在当前模式下，所有的测试用例仍然是以代码形式存在的，新增和修改测试用例时都需要到工程目录下编辑 Ruby 文件。&lt;/p&gt;

&lt;p&gt;那有没有一种可能，我们只需要在表格中维护自动化测试用例（如下图），然后由代码来读取表格内容就可以自动执行测试呢？&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/testcase_login_and_logout.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;是的，这就是我们对测试框架进行⎡工程化⎦改造的下一个形态，也就是&lt;a href="https://github.com/debugtalk/AppiumBooster" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;AppiumBooster&lt;/code&gt;&lt;/a&gt;现在的样子。&lt;/p&gt;

&lt;p&gt;在下一篇文章中，我们再进行详细探讨。&lt;/p&gt;
&lt;h2 id="Read More ..."&gt;Read More ...&lt;/h2&gt;
&lt;p&gt;微信公众号：&lt;a href="http://debugtalk.com/images/wechat_qrcode.png" rel="nofollow" target="_blank" title=""&gt;DebugTalk&lt;/a&gt;
原文链接：&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-refactor-testcase-scripts/" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-refactor-testcase-scripts/&lt;/a&gt;
《从 0 到 1 搭建移动 App 功能自动化测试平台》系列：&lt;a href="http://debugtalk.com/tags/F0T1/" rel="nofollow" target="_blank"&gt;http://debugtalk.com/tags/F0T1/&lt;/a&gt;&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Fri, 29 Jul 2016 09:46:28 +0800</pubDate>
      <link>https://ruby-china.org/topics/30666</link>
      <guid>https://ruby-china.org/topics/30666</guid>
    </item>
    <item>
      <title>关于持续集成打包平台的 Jenkins 配置和构建脚本实现细节</title>
      <description>&lt;p&gt;在&lt;a href="http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins/" rel="nofollow" target="_blank" title=""&gt;《使用 Jenkins 搭建 iOS/Android 持续集成打包平台》&lt;/a&gt;一文中，我对如何使用 Jenkins 搭建 iOS/Android 持续集成打包平台的基础概念和实施流程进行了介绍。本文作为配套，对搭建持续集成打包平台中涉及到的执行命令、构建脚本（build.py），以及 Jenkins 的配置进行详细的补充说明。&lt;/p&gt;

&lt;p&gt;当然，如果你不关心技术实现细节，也可以完全不用理会，直接参照【开箱即用】部分按照步骤进行操作即可。&lt;/p&gt;
&lt;h2 id="关于iOS的构建"&gt;关于 iOS 的构建&lt;/h2&gt;
&lt;p&gt;对 iOS 源码进行构建，目标是要生成&lt;code&gt;.ipa&lt;/code&gt;文件，即 iOS 应用安装包。&lt;/p&gt;

&lt;p&gt;当前，构建方式主要包括两种：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;源码&lt;/code&gt; -&amp;gt; &lt;code&gt;.archive&lt;/code&gt;文件 -&amp;gt; &lt;code&gt;.ipa&lt;/code&gt;文件&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;源码&lt;/code&gt; -&amp;gt; &lt;code&gt;.app&lt;/code&gt;文件 -&amp;gt; &lt;code&gt;.ipa&lt;/code&gt;文件&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这两种方式的主要差异是生成的中间产物不同，对应的，两种构建方式采用的命令也不同。&lt;/p&gt;
&lt;h3 id="源码 -&gt; .archive -&gt; .ipa"&gt;
&lt;code&gt;源码&lt;/code&gt; -&amp;gt; &lt;code&gt;.archive&lt;/code&gt; -&amp;gt; &lt;code&gt;.ipa&lt;/code&gt;
&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# build archive file from source code&lt;/span&gt;
xcodebuild &lt;span class="se"&gt;\ &lt;/span&gt;   &lt;span class="c"&gt;# xctool&lt;/span&gt;
  &lt;span class="nt"&gt;-workspace&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKSPACE_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-scheme&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SCHEME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-configuration&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIGURATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-sdk&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SDK&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  archive &lt;span class="nt"&gt;-archivePath&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;archive_path&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;archive&lt;/code&gt;：打包命令，会生成一个&lt;code&gt;.xcarchive&lt;/code&gt;的文件；archive 命令需要接一个参数&lt;code&gt;-archivePath&lt;/code&gt;，即存放 Archive 文件的目录&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# export ipa file from .archive&lt;/span&gt;
xcodebuild &lt;span class="nt"&gt;-exportArchive&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-exportFormat&lt;/span&gt; format &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-archivePath&lt;/span&gt; xcarchivepath &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-exportPath&lt;/span&gt; destinationpath &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-exportProvisioningProfile&lt;/span&gt; profilename &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;-exportSigningIdentity&lt;/span&gt; identityname]
  &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;-exportInstallerIdentity&lt;/span&gt; identityname]
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="源码 -&gt; .app -&gt; .ipa"&gt;
&lt;code&gt;源码&lt;/code&gt; -&amp;gt; &lt;code&gt;.app&lt;/code&gt; -&amp;gt; &lt;code&gt;.ipa&lt;/code&gt;
&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# build .app file from source code&lt;/span&gt;
xcodebuild &lt;span class="se"&gt;\ &lt;/span&gt;   &lt;span class="c"&gt;# xctool&lt;/span&gt;
  &lt;span class="nt"&gt;-workspace&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKSPACE_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-scheme&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SCHEME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-configuration&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIGURATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-sdk&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SDK&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;-derivedDataPath&lt;/span&gt; build
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# convert .app file to ipa file&lt;/span&gt;
xcrun &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-sdk&lt;/span&gt; iphoneos &lt;span class="se"&gt;\&lt;/span&gt;
  PackageApplication &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; build/Release-iphoneos/xxx.app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; build/Release-iphoneos/xxx.ipa
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="参数说明"&gt;参数说明&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;xcodebuild/xctool参数&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-workspace&lt;/code&gt;：需要打包的 workspace，后面接的文件一定要是&lt;code&gt;.xcworkspace&lt;/code&gt;结尾的；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-scheme&lt;/code&gt;：需要打包的 Scheme，一般与&lt;code&gt;$project_name&lt;/code&gt;相同；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-sdk&lt;/code&gt;：区分 iphone device 和 Simulator；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-configuration&lt;/code&gt;：需要打包的配置文件，我们一般在项目中添加多个配置，适合不同的环境，Release/Debug；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-derivedDataPath&lt;/code&gt;：指定编译结果文件的存储路径；例如，指定&lt;code&gt;-derivedDataPath build&lt;/code&gt;时，将在项目根目录下创建一个&lt;code&gt;build&lt;/code&gt;文件夹，生成的&lt;code&gt;.app&lt;/code&gt;文件将位于&lt;code&gt;build/Build/Products/Release-iphoneos&lt;/code&gt;中。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;除了采用官方的&lt;code&gt;xcodebuild&lt;/code&gt;命令，还可以使用由 Facebook 开发维护的&lt;code&gt;xctool&lt;/code&gt;。&lt;code&gt;xctool&lt;/code&gt;命令的使用方法基本与&lt;code&gt;xcodebuild&lt;/code&gt;一致，但是输出的日志会清晰很多，而且还有许多其它优化，详情请参考&lt;code&gt;xctool&lt;/code&gt;的官方文档。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;xcrun 参数&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-v&lt;/code&gt;：指定&lt;code&gt;.app&lt;/code&gt;文件的路径&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-o&lt;/code&gt;：指定生成&lt;code&gt;.ipa&lt;/code&gt;文件的路径&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="补充说明"&gt;补充说明&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1、获取 Targets、Schemes、Configurations 参数&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在填写&lt;code&gt;target&lt;/code&gt;/&lt;code&gt;workspace&lt;/code&gt;/&lt;code&gt;scheme&lt;/code&gt;/&lt;code&gt;configuration&lt;/code&gt;等参数时，如果不知道该怎么填写，可以在项目根目录下执行&lt;code&gt;xcodebuild -list&lt;/code&gt;命令，它会列出当前项目的所有可选参数。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  Store_iOS git:&lt;span class="o"&gt;(&lt;/span&gt;NPED&lt;span class="o"&gt;)&lt;/span&gt; ✗ xcodebuild &lt;span class="nt"&gt;-list&lt;/span&gt;
Information about project &lt;span class="s2"&gt;"Store"&lt;/span&gt;:
    Targets:
        Store
        StoreCI

    Build Configurations:
        Debug
        Release

    If no build configuration is specified and &lt;span class="nt"&gt;-scheme&lt;/span&gt; is not passed &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;"Release"&lt;/span&gt; is used.

    Schemes:
        Store
        StoreCI
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2、清除缓存文件&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在每次 build 之后，工程目录下会遗留一些缓存文件，以便下次 build 时减少编译时间。然而，若因为工程配置错误等问题造成编译失败后，下次再编译时就可能会受到缓存的影响。&lt;/p&gt;

&lt;p&gt;因此，在持续集成构建脚本中，比较好的做法是在每次 build 之前都清理一下上一次编译遗留的缓存文件。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# clean before build&lt;/span&gt;
xctool &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-workspace&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKSPACE_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-scheme&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SCHEME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-configuration&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIGURATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;clean&lt;/code&gt;：清除编译产生的问题，下次编译就是全新的编译了&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3、处理 Cocoapod 依赖库&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;另外一个需要注意的是，若项目是采用 Cocoapod 管理项目依赖，每次拉取最新代码后直接编译可能会报错。这往往是因为其他同事更新了依赖库（新增了第三方库或升级了某些库），而本地还采用之前的第三方库进行编译，从而会出现依赖库缺失或版本不匹配等问题。&lt;/p&gt;

&lt;p&gt;应对的做法是，在每次 build 之前都更新一下 Cocoapod。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update pod repository&lt;/span&gt;
pod repo update
&lt;span class="c"&gt;# Install pod dependencies&lt;/span&gt;
pod &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4、修改编译包的版本号&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;通过持续集成打包，我们会得到大量的安装包。为了便于区分，比较好的做法是在 App 中显示版本号，并将版本号与 Jenkins 的&lt;code&gt;BUILD_NUMBER&lt;/code&gt;关联起来。&lt;/p&gt;

&lt;p&gt;例如，当前项目的主版本号为&lt;code&gt;2.6.0&lt;/code&gt;，本次构建的&lt;code&gt;BUILD_NUMBER&lt;/code&gt;为 130，那么我们就可以将本次构建的 App 版本号设置为&lt;code&gt;2.6.0.130&lt;/code&gt;。通过这种方式，我们可以通过 App 中显示的版本号快速定位到具体到构建历史，从而对应到具体的代码提交记录。&lt;/p&gt;

&lt;p&gt;要实现对 App 版本号的设置，只需要在打包前对&lt;code&gt;Info.plist&lt;/code&gt;文件中的&lt;code&gt;CFBundleVersion&lt;/code&gt;和&lt;code&gt;CFBundleShortVersionString&lt;/code&gt;进行修改即可。在 Python 中，利用&lt;code&gt;plistlib&lt;/code&gt;库可以很方便地实现对&lt;code&gt;Info.plist&lt;/code&gt;文件的读写。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5、模拟器运行&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;如果持续集成测试是要运行在 iOS 模拟器上，那么就需要构建生成&lt;code&gt;.app&lt;/code&gt;文件。&lt;/p&gt;

&lt;p&gt;在前面讲解的两种构建方式中，中间产物都包含了&lt;code&gt;.app&lt;/code&gt;文件。对于以&lt;code&gt;.xcarchive&lt;/code&gt;为中间产物的方式，生成的&lt;code&gt;.app&lt;/code&gt;文件位于&lt;code&gt;output_dir/StoreCI_Release.xcarchive/Products/Applications/&lt;/code&gt;目录中。&lt;/p&gt;

&lt;p&gt;不过，这个&lt;code&gt;.app&lt;/code&gt;文件在模拟器中还无法直接运行，还需要在 Xcode 中修改&lt;code&gt;Supported Platforms&lt;/code&gt;，例如，将&lt;code&gt;iphoneos&lt;/code&gt;更改为&lt;code&gt;iOS&lt;/code&gt;。详细原因请参考&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-Appium-inspector-iOS-simulator/" rel="nofollow" target="_blank" title=""&gt;《从 0 到 1 搭建移动 App 功能自动化测试平台（1）：模拟器中运行 iOS 应用》&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="关于Android的构建"&gt;关于 Android 的构建&lt;/h2&gt;
&lt;p&gt;待续&lt;/p&gt;
&lt;h2 id="关于构建脚本"&gt;关于构建脚本&lt;/h2&gt;
&lt;p&gt;对于构建脚本（&lt;a href="https://github.com/debugtalk/JenkinsTemplateForApp/blob/master/workspace/YourProject/Build_scripts/build.py" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;build.py&lt;/code&gt;&lt;/a&gt;）本身，源码应该是最好的说明文档。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;build.py&lt;/code&gt;脚本中，主要实现的功能就四点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;执行构建命令，编译生成&lt;code&gt;.ipa&lt;/code&gt;文件，这部分包含了&lt;code&gt;关于iOS的构建&lt;/code&gt;部分的全部内容；&lt;/li&gt;
&lt;li&gt;构建时动态修改&lt;code&gt;Info.plist&lt;/code&gt;，将编译包的版本号与 Jenkins 的 BuildNumber 关联起来；&lt;/li&gt;
&lt;li&gt;上传&lt;code&gt;.ipa&lt;/code&gt;文件至&lt;code&gt;pyger&lt;/code&gt;/&lt;code&gt;fir.im&lt;/code&gt;平台，并且做了失败重试机制；&lt;/li&gt;
&lt;li&gt;解析&lt;code&gt;pyger&lt;/code&gt;/&lt;code&gt;fir.im&lt;/code&gt;平台页面中的二维码，将二维码图片保存到本地。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;需要说明的是，对于构建任务中常用的可配置参数，例如&lt;code&gt;BRANCH&lt;/code&gt;/&lt;code&gt;SCHEME&lt;/code&gt;/&lt;code&gt;CONFIGURATION&lt;/code&gt;/&lt;code&gt;OUTPUT_FOLDER&lt;/code&gt;等，需要在构建脚本中通过&lt;code&gt;OptionParser&lt;/code&gt;的方式实现可传参数机制。这样我们不仅可以命令行中通过传参的方式灵活地调用构建脚本，也可以在 Jenkins 中实现参数传递。&lt;/p&gt;

&lt;p&gt;之所以强调&lt;code&gt;常用的&lt;/code&gt;可配置参数，这是为了尽可能减少参数数目，降低脚本调用的复杂度。像&lt;code&gt;PROVISIONING_PROFILE&lt;/code&gt;和&lt;code&gt;pgyer/fir.im&lt;/code&gt;账号这种比较固定的配置参数，就可以写死在脚本中。因此，在使用构建脚本（build.py）之前，需要先在脚本中配置下&lt;code&gt;PROVISIONING_PROFILE&lt;/code&gt;和&lt;code&gt;pgyer/fir.im&lt;/code&gt;账号。&lt;/p&gt;

&lt;p&gt;另外还想多说一句，&lt;code&gt;pyger&lt;/code&gt;/&lt;code&gt;fir.im&lt;/code&gt;这类第三方平台在为我们提供便利的同时，稳定性不可控也是一个不得不考虑的问题。在我使用&lt;code&gt;pgyer&lt;/code&gt;平台期间，就遇到了平台服务变动、接口时而不稳定出现 502 等问题。因此，最好的方式还是自行搭建一套类似的服务，反正我是打算这么做了。&lt;/p&gt;
&lt;h2 id="Jenkins的详细配置"&gt;Jenkins 的详细配置&lt;/h2&gt;
&lt;p&gt;对于 Jenkins 的详细配置，需要补充说明的有四点。&lt;/p&gt;
&lt;h3 id="1、参数的传递"&gt;1、参数的传递&lt;/h3&gt;
&lt;p&gt;在构建脚本中，我们已经对常用的可配置参数实现了可传参机制。例如，在 Terminal 中可以通过如下形式调用构建脚本。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python build.py &lt;span class="nt"&gt;--scheme&lt;/span&gt; SCHEME &lt;span class="nt"&gt;--workspace&lt;/span&gt; Store.xcworkspace &lt;span class="nt"&gt;--configuration&lt;/span&gt; CONFIGURATION &lt;span class="nt"&gt;--output&lt;/span&gt; OUTPUT_FOLDER
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么我们在 Jenkins 中要怎样才能指定参数呢？&lt;/p&gt;

&lt;p&gt;实际上，Jenkins 针对项目具有参数化的功能。在项目的配置选项中，勾选&lt;code&gt;This project is parameterized&lt;/code&gt;后，就可以为当前 project 添加多种类型的参数，包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Boolean Parameter&lt;/li&gt;
&lt;li&gt;Choice Parameter&lt;/li&gt;
&lt;li&gt;Credentials Parameter&lt;/li&gt;
&lt;li&gt;File Parameter&lt;/li&gt;
&lt;li&gt;Multi-line String Parameter&lt;/li&gt;
&lt;li&gt;Password Parameter&lt;/li&gt;
&lt;li&gt;Run Parameter&lt;/li&gt;
&lt;li&gt;String Parameter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;通常，我们可以选择使用&lt;code&gt;String Parameter&lt;/code&gt;来定义自定义参数，并可对每个参数设置默认值。&lt;/p&gt;

&lt;p&gt;当我们配置了&lt;code&gt;BRANCH&lt;/code&gt;、&lt;code&gt;SCHEME&lt;/code&gt;、&lt;code&gt;CONFIGURATION&lt;/code&gt;、&lt;code&gt;OUTPUT_FOLDER&lt;/code&gt;、&lt;code&gt;BUILD_VERSION&lt;/code&gt;这几个参数后，我们就可以在&lt;code&gt;Build&lt;/code&gt;配置区域的&lt;code&gt;Execute shell&lt;/code&gt;通过如下形式来进行参数传递。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKSPACE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/Build_scripts/build.py &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--scheme&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SCHEME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--workspace&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKSPACE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/Store.xcworkspace &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--configuration&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIGURATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKSPACE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OUTPUT_FOLDER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--build_version&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUILD_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUILD_NUMBER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，参数的传递方式很简单，只需要预先定义好了自定义参数，然后就可以通过&lt;code&gt;${Param}&lt;/code&gt;的形式来进行调用了。&lt;/p&gt;

&lt;p&gt;不过你也许会问，&lt;code&gt;WORKSPACE&lt;/code&gt;和&lt;code&gt;BUILD_NUMBER&lt;/code&gt;这两个参数我们并未进行定义，为什么也能进行调用呢？这是因为 Jenkins 自带部分与项目相关的环境变量，例如&lt;code&gt;BRANCH_NAME&lt;/code&gt;、&lt;code&gt;JOB_NAME&lt;/code&gt;等，这部分参数可以在 shell 脚本中直接进行调用。完整的环境变量可在&lt;code&gt;Jenkins_Url/env-vars.html/&lt;/code&gt;中查看。&lt;/p&gt;

&lt;p&gt;配置完成后，就可以在&lt;code&gt;Build with Parameters&lt;/code&gt;中通过如下形式手动触发构建。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/Jenkins_manul_build.jpg" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="2、修改build名称"&gt;2、修改 build 名称&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;Build History&lt;/code&gt;列表中，构建任务的名称默认显示为按照 build 次数递增的&lt;code&gt;BUILD_NUMBER&lt;/code&gt;。有时候我们可能想在 build 名称中包含更多的信息，例如包含当次构建的&lt;code&gt;SCHEME&lt;/code&gt;和&lt;code&gt;CONFIGURATION&lt;/code&gt;，这时我们就可以通过修改&lt;code&gt;BuildName&lt;/code&gt;实现。&lt;/p&gt;

&lt;p&gt;Jenkins 默认不支持&lt;code&gt;BuildName&lt;/code&gt;设置，但可通过安装&lt;code&gt;build-name-setter&lt;/code&gt;插件进行实现。安装&lt;code&gt;build-name-setter&lt;/code&gt;插件后，在配置页面的&lt;code&gt;Build Environment&lt;/code&gt;栏目下会出现&lt;code&gt;Set Build Name&lt;/code&gt;配置项，然后在&lt;code&gt;Build Name&lt;/code&gt;中就可以通过环境变量参数来设置 build 名称。&lt;/p&gt;

&lt;p&gt;例如，要将 build 名称设置为上面截图中的&lt;code&gt;StoreCI_Release_#130&lt;/code&gt;样式，就可以在&lt;code&gt;Build Name&lt;/code&gt;中配置为&lt;code&gt;${SCHEME}_${CONFIGURATION}_#${BUILD_NUMBER}&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;除了在&lt;code&gt;Build Name&lt;/code&gt;中传递环境变量参数，&lt;code&gt;build-name-setter&lt;/code&gt;还可以实现许多更加强大的自定义功能，大家可自行探索。&lt;/p&gt;
&lt;h3 id="3、展示二维码图片"&gt;3、展示二维码图片&lt;/h3&gt;
&lt;p&gt;然后再说下如何在&lt;code&gt;Build History&lt;/code&gt;列表中展示每次构建对应的二维码图片。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/Jenkins_build_history.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;需要说明的是，在上图中，绿色框对应的内容是&lt;code&gt;BuildName&lt;/code&gt;，我们可以通过&lt;code&gt;build-name-setter&lt;/code&gt;插件来实现自定义配置；但是红色框已经不在&lt;code&gt;BuildName&lt;/code&gt;的范围之内，而是对应的&lt;code&gt;BuildDescription&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;同样地，Jenkins 默认不支持在构建过程中自动修改&lt;code&gt;BuildDescription&lt;/code&gt;，需要通过安装&lt;code&gt;description setter plugin&lt;/code&gt;插件来辅助实现。安装&lt;code&gt;description setter plugin&lt;/code&gt;插件后，在配置页面的&lt;code&gt;Build&lt;/code&gt;栏目下，&lt;code&gt;Add build step&lt;/code&gt;中会出现&lt;code&gt;Set build description&lt;/code&gt;配置项，添加该配置项后就会出现如下配置框。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/Jenkins_set_build_description.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;该功能的强大之处在于，它可以在构建日志中通过正则表达式来匹配内容，并将匹配到的内容添加到&lt;code&gt;BuildDescription&lt;/code&gt;中去。&lt;/p&gt;

&lt;p&gt;例如，我们想要展示的二维码图片是在每次构建过程中生成的，因此我们首先要获取到二维码图片文件。&lt;/p&gt;

&lt;p&gt;我的做法是，在&lt;code&gt;build.py&lt;/code&gt;中将蒲公英平台返回的应用下载页面地址和二维码图片地址打印到 log 中。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;appDownloadPage: https://www.pgyer.com/035aaf10acf5dd7c279c4fe423a57674
appQRCodeURL: https://o1wjx1evz.qnssl.com/app/qrcodeHistory/fe7a8c9051f0c7fc0affc78f40c20a4b5e4bdb4c77b91a29501f55fd9039c659
Save QRCode image to file: /Users/Leo/.jenkins/workspace/DJI_Plus_Store_iOS/build_outputs/QRCode.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，在&lt;code&gt;Set build description&lt;/code&gt;配置项的&lt;code&gt;Regular expression&lt;/code&gt;就可以按照如下正则表达式进行匹配：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;appDownloadPage: (.*)$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，就可以在&lt;code&gt;Description&lt;/code&gt;中对匹配到的结果进行引用。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img src='${BUILD_URL}artifact/build_outputs/QRCode.png'&amp;gt;\n&amp;lt;a href='\1'&amp;gt;Install Online&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，我们用到了 HTML 的标签，而 Jenkins 的&lt;code&gt;Markup Formatter&lt;/code&gt;默认是采用&lt;code&gt;Plain text&lt;/code&gt;模式，因此还需要对 Jenkins 对系统配置进行修改，在&lt;a href="http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins/" rel="nofollow" target="_blank" title=""&gt;《使用 Jenkins 搭建 iOS/Android 持续集成打包平台》&lt;/a&gt;中已进行了详细说明，在此就不再重复。&lt;/p&gt;

&lt;p&gt;通过以上方式，就可以实现前面图片中的效果。&lt;/p&gt;
&lt;h3 id="4、收集编译成果物"&gt;4、收集编译成果物&lt;/h3&gt;
&lt;p&gt;在上面讲解的展示二维码图片一节中，用到了&lt;code&gt;${BUILD_URL}artifact/build_outputs/QRCode.png&lt;/code&gt;一项，这里的 URL 就是用到了编译成果物收集后保存的路径。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Archives build artifacts&lt;/code&gt;是 Jenkins 默认自带的功能，无需安装插件。该功能在配置页面的&lt;code&gt;Post-build Actions&lt;/code&gt;栏目下，在&lt;code&gt;Add post-build action&lt;/code&gt;的列表中选择添加&lt;code&gt;Archives build artifacts&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;添加后的配置页面如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/Jenkins_archive_the_artifacts.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;通常，我们只需要配置&lt;code&gt;Files to archive&lt;/code&gt;即可。定位文件时，可以通过正则表达式进行匹配，也可以调用项目的环境变量；多个文件通过逗号进行分隔。&lt;/p&gt;

&lt;p&gt;例如，假如我们想收集&lt;code&gt;QRCode.png&lt;/code&gt;、&lt;code&gt;StoreCI_Release.ipa&lt;/code&gt;、&lt;code&gt;Info.plist&lt;/code&gt;这三个文件，那么我们就可以通过如下表达式来进行指定。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${OUTPUT_FOLDER}/*.ipa,${OUTPUT_FOLDER}/QRCode.png,${OUTPUT_FOLDER}/*.xcarchive/Info.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，目标文件的具体位置是我们在构建脚本（&lt;code&gt;build.py&lt;/code&gt;）中预先进行处理的。&lt;/p&gt;

&lt;p&gt;通过这种方式，我们就可以实现在每次完成构建后将需要的文件收集起来进行存档，以便后续在 Jenkins 的任务页面中进行下载。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/images/Jenkins_show_artifacts.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;也可以直接通过归档文件的 URL 进行访问。例如，上图中&lt;code&gt;QRCode.png&lt;/code&gt;的 URL 为&lt;code&gt;Jenkins_Url/job/JenkinsJobName/131/artifact/build_outputs/QRCode.png&lt;/code&gt;，而&lt;code&gt;Jenkins_Url/job/JenkinsJobName/131/&lt;/code&gt;即是&lt;code&gt;${BUILD_URL}&lt;/code&gt;，因此可以直接通过&lt;code&gt;${BUILD_URL}artifact/build_outputs/QRCode.png&lt;/code&gt;引用。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;至此，&lt;a href="http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins/" rel="nofollow" target="_blank" title=""&gt;《使用 Jenkins 搭建 iOS/Android 持续集成打包平台》&lt;/a&gt;一文中涉及到的 Jenkins 配置和构建脚本实现细节均已补充完毕了。相信大家结合这两篇文章，应该会对如何使用 Jenkins 搭建 iOS/Android 持续集成打包平台的基础概念和实现细节都有一个比较清晰的认识。&lt;/p&gt;

&lt;p&gt;对于还未完善的部分，我后续将在博客中进行更新。&lt;/p&gt;

&lt;p&gt;操作手册请参考文章末尾的【开箱即用】部分，祝大家玩得愉快！&lt;/p&gt;
&lt;h2 id="开箱即用"&gt;开箱即用&lt;/h2&gt;
&lt;p&gt;GitHub 地址：&lt;a href="https://github.com/debugtalk/JenkinsTemplateForApp" rel="nofollow" target="_blank"&gt;https://github.com/debugtalk/JenkinsTemplateForApp&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="1、添加构建脚本"&gt;1、添加构建脚本&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在构建脚本中配置&lt;code&gt;PROVISIONING_PROFILE&lt;/code&gt;和&lt;code&gt;pgyer/fir.im&lt;/code&gt;账号；&lt;/li&gt;
&lt;li&gt;在目标构建代码库的根目录中，创建&lt;code&gt;Build_scripts&lt;/code&gt;文件夹，并将&lt;code&gt;build.py&lt;/code&gt;拷贝到&lt;code&gt;Build_scripts&lt;/code&gt;中；&lt;/li&gt;
&lt;li&gt;将&lt;code&gt;Build_scripts/build.py&lt;/code&gt;提交到项目中。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;除了与 Jenkins 实现持续集成，构建脚本还可单独使用，使用方式如下：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKSPACE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/Build_scripts/build.py &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--scheme&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SCHEME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--workspace&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKSPACE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/Store.xcworkspace &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--configuration&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIGURATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WORKSPACE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OUTPUT_FOLDER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2、运行jenkins，安装必备插件"&gt;2、运行 jenkins，安装必备插件&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;nohup &lt;/span&gt;java &lt;span class="nt"&gt;-jar&lt;/span&gt; jenkins_located_path/jenkins.war &amp;amp;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="3、创建Jenkins Job"&gt;3、创建 Jenkins Job&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在 Jenkins 中创建一个&lt;code&gt;Freestyle project&lt;/code&gt;类型的 Job，先不进行任何配置；&lt;/li&gt;
&lt;li&gt;然后将&lt;code&gt;config.xml&lt;/code&gt;文件拷贝到&lt;code&gt;~/.jenkins/jobs/YourProject/&lt;/code&gt;中覆盖原有配置文件，重启 Jenkins；&lt;/li&gt;
&lt;li&gt;完成配置文件替换和重启后，刚创建好的 Job 就已完成了大部分配置；&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;Job Configure&lt;/code&gt;中根据项目实际情况调整配置，其中&lt;code&gt;Git Repositories&lt;/code&gt;是必须修改的，其它配置项可选择性地进行调整。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4、done！"&gt;4、done！&lt;/h3&gt;&lt;h2 id="Read More ..."&gt;Read More ...&lt;/h2&gt;
&lt;p&gt;微信公众号：&lt;a href="http://debugtalk.com/images/wechat_qrcode.png" rel="nofollow" target="_blank" title=""&gt;DebugTalk&lt;/a&gt;
原文链接：&lt;a href="http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins-details" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins-details&lt;/a&gt;
构建脚本&amp;amp;配置文件：&lt;a href="https://github.com/debugtalk/JenkinsTemplateForApp" rel="nofollow" target="_blank"&gt;https://github.com/debugtalk/JenkinsTemplateForApp&lt;/a&gt;&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Tue, 26 Jul 2016 21:29:45 +0800</pubDate>
      <link>https://ruby-china.org/topics/30632</link>
      <guid>https://ruby-china.org/topics/30632</guid>
    </item>
    <item>
      <title>基于 Ruby 实现的移动 App 自动化测试框架</title>
      <description>&lt;h2 id="AppiumBooster"&gt;AppiumBooster&lt;/h2&gt;
&lt;p&gt;AppiumBooster helps you to write automation testcases in tables, without writing a snippet of code.&lt;/p&gt;
&lt;h2 id="write testcases"&gt;write testcases&lt;/h2&gt;
&lt;p&gt;You can write testcases in any table tools, including MS Excel and iWork Numbers, and even in plain CSV format.&lt;/p&gt;

&lt;p&gt;Take DJI+ Discover's login and logout function as an example.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://raw.githubusercontent.com/debugtalk/AppiumBooster/master/examples/preview_login_and_logout.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;In order to test these functions above, you can write testcases in tables like this.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://raw.githubusercontent.com/debugtalk/AppiumBooster/master/examples/testcase_login_and_logout.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;After the testcases are finished, export to CSV format, and put the csv files under &lt;code&gt;ios/testcases/&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;That's all you need to do, and now you are ready to run automation test on your app.&lt;/p&gt;
&lt;h2 id="run"&gt;run&lt;/h2&gt;
&lt;p&gt;Run the automation testcases is very easy. Just execute &lt;code&gt;ruby run.rb&lt;/code&gt; in the project root directory.&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  AppiumBooster git:&lt;span class="o"&gt;(&lt;/span&gt;master&lt;span class="o"&gt;)&lt;/span&gt; ✗ ruby run.rb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AppiumBooster will load all the csv test suites and then excute each suite sequentially.&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;➜  AppiumBooster git:(master) ✗ ruby run.rb
initialize appium driver ...
start appium driver ...
alert accepted!
======= start to run testcase suite: ./ios/testcases/Account-Login and Logout.csv =======
load csv testcase file: ./ios/testcases/Account-Login and Logout.csv ...
B------ Start to run testcase: Login with valid account
step_1: Enter My Account Page
control_element.click    ...    ✓
uiviewMyAccount exsits?    ...    ✓
step_2: Enter Login Page
control_element.click    ...    ✓
uiviewLogIn exsits?    ...    ✓
step_3: Input Email Address
control_element.type 'leo.lee@dji.com'    ...    ✓
step_4: Input Password
control_element.type '123456'    ...    ✓
step_5: Login
control_element.click    ...    ✓
tablecellMyMessage exsits?    ...    ✓
step_6: Check if coupon popup window exists
inner_screen.has_control 'btnViewMyCoupons'    ...    ✓
btnClose exsits?    ...    ✓
step_7: Close coupon popup window
control_element.click    ...    ✓
!btnClose no longer exsits?    ...    ✓
E------ Login with valid account

B------ Start to run testcase: Logout
step_1: Enter My Account Page
control_element.click    ...    ✓
uiviewMyAccount exsits?    ...    ✓
step_2: Enter System Settings
control_element.click    ...    ✓
btnLogout exsits?    ...    ✓
step_3: Click Logout
control_element.click    ...    ✓
Do you want to log out? exsits?    ...    ✓
step_4: Confirm logout alert
alert accepted!
alert_accept    ...    ✓
tablecellMyAccountLogin exsits?    ...    ✓
E------ Logout

============ all testcases have been executed. ============
quit appium driver.
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="Source Code"&gt;Source Code&lt;/h2&gt;
&lt;p&gt;GitHub: &lt;a href="http://debugtalk.com/post/Introduction-to-AppiumBooster/" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/Introduction-to-AppiumBooster/&lt;/a&gt;&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Thu, 21 Jul 2016 19:16:44 +0800</pubDate>
      <link>https://ruby-china.org/topics/30590</link>
      <guid>https://ruby-china.org/topics/30590</guid>
    </item>
    <item>
      <title>使用 Jenkins 搭建 iOS/Android 持续集成打包平台 (附 GitHub，开箱即用)</title>
      <description>&lt;h2 id="背景描述"&gt;背景描述&lt;/h2&gt;
&lt;p&gt;根据项目需求，现要在团队内部搭建一个统一的打包平台，实现对 iOS 和 Android 项目的打包。而且为了方便团队内部的测试包分发，希望在打包完成后能生成一个二维码，体验用户（产品、运营、测试等人员）通过手机扫描二维码后就能直接安装测试包。&lt;/p&gt;

&lt;p&gt;该需求具有一定的普遍性，基本上所有开发 APP 的团队都可能会用到，因此我将整个需求实现的过程整理后形成此文，并且真正地做到了&lt;code&gt;零基础上手，到手即飞、开箱即用&lt;/code&gt;，希望能对大家有所帮助。&lt;/p&gt;

&lt;p&gt;首先，先给大家展示下平台建设完成后的整体效果：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/Jenkins_Job_Overview.jpg" title="" alt=""&gt;
&lt;img src="http://debugtalk.com/assets/images/Jenkins_Job_Build_View.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;该平台主要实现的功能有 3 点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;定期对 GitHub 仓库进行检测，若有更新则自动执行构建打包；&lt;/li&gt;
&lt;li&gt;构建成功后根据 ipa/apk 生成二维码，并可在历史构建列表中展示各个版本的二维码，通过手机扫描二维码可直接安装对应版本；&lt;/li&gt;
&lt;li&gt;在构建结果页面中展示当次构建的成果物（Artifact，如&lt;code&gt;.ipa&lt;/code&gt;、&lt;code&gt;.app&lt;/code&gt;、&lt;code&gt;.apk&lt;/code&gt;、&lt;code&gt;info.plist&lt;/code&gt;等文件），供有需要的用户进行下载。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接下来，本文就开始对平台建设的完整实现过程进行详细介绍。&lt;/p&gt;
&lt;h2 id="安装Jenkins"&gt;安装 Jenkins&lt;/h2&gt;
&lt;p&gt;Jenkins 依赖于 Java 运行环境，因此需要首先安装&lt;a href="http://www.oracle.com/technetwork/java/javase/downloads/index.html" rel="nofollow" target="_blank" title=""&gt;Java&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;安装 Jenkins 的方式有多种，可以运行对应系统类型的安装包，可以通过 docker 获取镜像，也可以直接运行&lt;code&gt;war&lt;/code&gt;包。&lt;/p&gt;

&lt;p&gt;我个人倾向于直接运行&lt;code&gt;war&lt;/code&gt;包的形式，只需下载&lt;code&gt;jenkins.war&lt;/code&gt;后，运行如下命令即可启动 Jenkins。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;nohup &lt;/span&gt;java &lt;span class="nt"&gt;-jar&lt;/span&gt; jenkins_located_path/jenkins.war &lt;span class="nt"&gt;--httpPort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;88 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不指定&lt;code&gt;httpPort&lt;/code&gt;，Jenkins 的默认端口为 8080。&lt;/p&gt;
&lt;h2 id="Jenkins插件"&gt;Jenkins 插件&lt;/h2&gt;
&lt;p&gt;Jenkins 有非常多的插件，可以实现各种功能的扩展。&lt;/p&gt;

&lt;p&gt;针对搭建的 iOS/Android 持续集成打包平台，我使用到了如下几个插件。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GIT plugin&lt;/li&gt;
&lt;li&gt;SSH Credentials Plugin&lt;/li&gt;
&lt;li&gt;Git Changelog Plugin: 获取仓库提交的 commit log&lt;/li&gt;
&lt;li&gt;build-name-setter：用于修改 Build 名称&lt;/li&gt;
&lt;li&gt;description setter plugin：用于在修改 Build 描述信息，在描述信息中增加显示 QRCode（二维码）&lt;/li&gt;
&lt;li&gt;Post-Build Script Plug-in：在编译完成后通过执行脚本实现一些额外功能&lt;/li&gt;
&lt;li&gt;Xcode integration: iOS 专用（可选）&lt;/li&gt;
&lt;li&gt;Gradle plugin: Android 专用（可选）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;安装方式也比较简单，直接在 Jenkins 的插件管理页面搜索上述插件，点击安装即可。&lt;/p&gt;
&lt;h2 id="创建项目（Job）"&gt;创建项目（Job）&lt;/h2&gt;
&lt;p&gt;在 Jenkins 中，构建项目以 Job 的形式存在，因此需要针对每个项目创建一个 Job。有时候，一个项目中可能有多个分支同时在进行开发，为了分别进行构建，也可以针对每个分支创建一个 Job。&lt;/p&gt;

&lt;p&gt;创建 Job 的方式有多种，本次只需要创建&lt;code&gt;Freestyle project&lt;/code&gt;类型的即可。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Main page&lt;/code&gt; -&amp;gt; &lt;code&gt;New Item&lt;/code&gt; -&amp;gt; &lt;code&gt;Freestyle project&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;对于一个持续集成打包平台，每次打包都由 4 步组成：触发构建、拉取代码、执行构建、构建后处理。对应的，在每个 Job 中也对应了这几项的配置。&lt;/p&gt;
&lt;h2 id="配置Git代码仓库"&gt;配置 Git 代码仓库&lt;/h2&gt;
&lt;p&gt;要对项目进行构建，配置项目的代码仓库是必不可少的。由于当前我们的项目托管在 GitHub 私有仓库中，因此在此需要对&lt;code&gt;Git&lt;/code&gt;进行配置。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;【Source Code Management】&lt;/code&gt;配置栏目下，如果之前&lt;code&gt;GIT plugin&lt;/code&gt;安装成功，则会出现&lt;code&gt;Git&lt;/code&gt;选项。&lt;/p&gt;

&lt;p&gt;配置 Git 代码仓库时，有三项是必须配置的：仓库 URL 地址（&lt;code&gt;Repository URL&lt;/code&gt;）、仓库权限校验方式（&lt;code&gt;Credentials&lt;/code&gt;），以及当前 Job 需要构建的代码分支（&lt;code&gt;Branches to build&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;在配置&lt;code&gt;Repository URL&lt;/code&gt;时，选择&lt;code&gt;HTTPS URL&lt;/code&gt;或&lt;code&gt;SSH URL&lt;/code&gt;均可。不过需要注意的是，&lt;code&gt;Credentials&lt;/code&gt;要和&lt;code&gt;Repository URL&lt;/code&gt;对应，也就是说：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果&lt;code&gt;Repository URL&lt;/code&gt;是&lt;code&gt;HTTPS URL&lt;/code&gt;形式的，那么&lt;code&gt;Credentials&lt;/code&gt;就要采用 GitHub 用户名密码的校验方式；而且，如果在 GitHub 中开启了&lt;code&gt;2FA（two-factor authentication）&lt;/code&gt;，那么还需要在 GitHub 中创建一个&lt;code&gt;Personal access token&lt;/code&gt;，输入密码时将这个&lt;code&gt;Personal access token&lt;/code&gt;作为密码进行输入。&lt;/li&gt;
&lt;li&gt;如果&lt;code&gt;Repository URL&lt;/code&gt;是&lt;code&gt;SSH URL&lt;/code&gt;形式的，那么就需要先在 Jenkins 所在的服务器上创建一个&lt;code&gt;SSH&lt;/code&gt;秘钥对，并将公钥添加到 GitHub 的&lt;code&gt;SSH keys&lt;/code&gt;中，然后在填写&lt;code&gt;Credentials&lt;/code&gt;时，选择&lt;code&gt;SSH Username with private key&lt;/code&gt;的校验方式，填入 GitHub Username、SSH 私钥、以及创建&lt;code&gt;SSH&lt;/code&gt;秘钥对时设置的&lt;code&gt;Passphrase&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果对 Git 权限校验的概念还比较模糊，可以参考《深入浅出 Git 权限校验》。&lt;/p&gt;

&lt;p&gt;在配置&lt;code&gt;Branches to build&lt;/code&gt;时，可以采用多种形式，包括分支名称（&lt;code&gt;branchName&lt;/code&gt;）、&lt;code&gt;tagName&lt;/code&gt;、&lt;code&gt;commitId&lt;/code&gt;等。其中分支名称的形式用的最多，例如，若是构建&lt;code&gt;master&lt;/code&gt;分支，则填写&lt;code&gt;refs/heads/master&lt;/code&gt;，若是构建&lt;code&gt;develop&lt;/code&gt;分支，则填写&lt;code&gt;refs/heads/develop&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;除了以上关于 Git 的必填配置项，有时根据项目的实际情况，可能还需要对 Jenkins 的默认配置项进行修改。&lt;/p&gt;

&lt;p&gt;比较常见的一种情况就是对&lt;code&gt;clone&lt;/code&gt;的配置进行修改。&lt;/p&gt;

&lt;p&gt;在 Jenkins 的默认配置中，&lt;code&gt;clone&lt;/code&gt;代码时会拉取所有历史版本的代码，而且默认的超时时限只有 10 分钟。这就造成在某些项目中，由于代码量本身就比较大，历史版本也比较多，再加上网络环境不是特别好，Jenkins 根本没法在 10 分钟之内拉取完所有代码，超时后任务就会被自动终止了（错误状态码 143）。&lt;/p&gt;

&lt;p&gt;这种问题的解决方式也很简单，无非就是两种思路，要么少拉取点代码（不获取历史版本），要么提高超时时限。对应的配置在&lt;code&gt;Advanced clone behaviours&lt;/code&gt;中：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Shallow clone&lt;/code&gt;：勾选后不获取历史版本；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Timeout (in minutes) for clone and fetch operation&lt;/code&gt;：配置后覆盖默认的超时时限。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="配置构建触发器"&gt;配置构建触发器&lt;/h2&gt;
&lt;p&gt;代码仓库配置好了，意味着 Jenkins 具有了访问 GitHub 代码仓库的权限，可以成功地拉取代码。&lt;/p&gt;

&lt;p&gt;那 Jenkins 什么时候执行构建呢？&lt;/p&gt;

&lt;p&gt;这就需要配置构建触发策略，即构建触发器，配置项位于&lt;code&gt;【Build Triggers】&lt;/code&gt;栏目。&lt;/p&gt;

&lt;p&gt;触发器支持多种类型，常用的有：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;定期进行构建（Build periodically）&lt;/li&gt;
&lt;li&gt;根据提交进行构建（Build when a change is pushed to GitHub）&lt;/li&gt;
&lt;li&gt;定期检测代码更新，如有更新则进行构建（Poll SCM）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;构建触发器的选择为复合选项，若选择多种类型，则任一类型满足构建条件时就会执行构建工作。如果所有类型都不选择，则该&lt;code&gt;Jenkins Job&lt;/code&gt;不执行自动构建，但可通过手动点击&lt;code&gt;【Build Now】&lt;/code&gt;触发构建。&lt;/p&gt;

&lt;p&gt;关于定时器（Schedule）的格式，简述如下：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MINUTE HOUR DOM MONTH DOW&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MINUTE: Minutes within the hour (0-59)&lt;/li&gt;
&lt;li&gt;HOUR: The hour of the day (0-23)&lt;/li&gt;
&lt;li&gt;DOM: The day of the month (1-31)&lt;/li&gt;
&lt;li&gt;MONTH: The month (1-12)&lt;/li&gt;
&lt;li&gt;DOW: The day of the week (0-7) where 0 and 7 are Sunday.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;通常情况下需要指定多个值，这时可以采用如下 operator（优先级从上到下）：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;*&lt;/code&gt;适配所有有效的值，若不指定某一项，则以&lt;code&gt;*&lt;/code&gt;占位；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;M-N&lt;/code&gt;适配值域范围，例如 7-9 代表 7/8/9 均满足；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;M-N/X&lt;/code&gt;或&lt;code&gt;*/X&lt;/code&gt;：以 X 作为间隔；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;A,B,C&lt;/code&gt;：枚举多个值。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外，为了避免多个任务在同一时刻同时触发构建，在指定时间段时可以配合使用&lt;code&gt;H&lt;/code&gt;字符。添加&lt;code&gt;H&lt;/code&gt;字符后，Jenkins 会在指定时间段内随机选择一个时间点作为起始时刻，然后加上设定的时间间隔，计算得到后续的时间点。直到下一个周期时，Jenkins 又会重新随机选择一个时间点作为起始时刻，依次类推。&lt;/p&gt;

&lt;p&gt;为了便于理解，列举几个示例：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;H/15 * * * *&lt;/code&gt;：代表每隔 15 分钟，并且开始时间不确定，这个小时可能是&lt;code&gt;:07,:22,:37,:52&lt;/code&gt;，下一个小时就可能是&lt;code&gt;:03,:18,:33,:48&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;H(0-29)/10 * * * *&lt;/code&gt;：代表前半小时内每隔 10 分钟，并且开始时间不确定，这个小时可能是&lt;code&gt;:04,:14,:24&lt;/code&gt;，下一个小时就可能是&lt;code&gt;:09,:19,:29&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;H 23 * * 1-5&lt;/code&gt;：工作日每晚 23:00 至 23:59 之间的某一时刻；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="配置构建方式"&gt;配置构建方式&lt;/h2&gt;
&lt;p&gt;触发策略配置好之后，Jenkins 就会按照设定的策略自动执行构建。但如何执行构建操作，这还需要我们通过配置构建方式来进行设定。&lt;/p&gt;

&lt;p&gt;常用的构建方式是根据构建对象的具体类型，安装对应的插件，然后采用相应的构建方式。例如，若是构建&lt;code&gt;Android&lt;/code&gt;应用，安装&lt;code&gt;Gradle plugin&lt;/code&gt;之后，就可以选择&lt;code&gt;Invoke Gradle script&lt;/code&gt;，然后采用&lt;code&gt;Gradle&lt;/code&gt;进行构建；若是构建&lt;code&gt;iOS&lt;/code&gt;应用，安装&lt;code&gt;Xcode integration&lt;/code&gt;插件之后，就可以选择&lt;code&gt;Xcode&lt;/code&gt;，然后选择&lt;code&gt;Xcode&lt;/code&gt;进行构建。&lt;/p&gt;

&lt;p&gt;该种方式的优势是操作简单，UI 可视化，在场景不复杂的情况下可以快速满足需求。不过缺点就是依赖于插件已有的功能，如果场景较复杂时可能单个插件还无法满足需求，需要再安装其它插件。而且，有些插件可能还存在一些问题，例如对某些操作系统版本或 XCode 版本兼容不佳，出现问题时我们就会比较被动。&lt;/p&gt;

&lt;p&gt;我个人更倾向于另外一种方式，就是自己编写打包脚本，在脚本中自定义实现所有的构建功能，然后在&lt;code&gt;Execute Shell&lt;/code&gt;中执行。这种方式的灵活度更高，各种场景的构建需求都能满足，出现问题后也能自行快速修复。&lt;/p&gt;

&lt;p&gt;另外，对于 iOS 应用的构建，还有一个需要额外关注的点，就是开发者证书的配置。&lt;/p&gt;

&lt;p&gt;如果是采用&lt;code&gt;Xcode integration&lt;/code&gt;插件进行构建，配置会比较复杂，需要在 Jenkins 中导入开发证书，并填写多个配置项。不过，如果是采用打包脚本进行构建的话，情况就会简单许多。只要在 Jenkins 所运行的计算机中安装好开发者证书，打包命令在 Shell 中能正常工作，那么在 Jenkins 中执行打包脚本也不会有什么问题。&lt;/p&gt;
&lt;h2 id="构建后处理"&gt;构建后处理&lt;/h2&gt;
&lt;p&gt;完成构建后，生成的编译成果物（ipa/apk）会位于指定的目录中。但是，如果要直接在手机中安装&lt;code&gt;ipa/apk&lt;/code&gt;文件还比较麻烦，不仅在分发测试包时需要将好几十兆的安装包进行传送，体验用户在安装时也还需要通过数据线将手机与计算机进行连接，然后再使用 PP 助手或豌豆荚等工具进行安装。&lt;/p&gt;

&lt;p&gt;当前比较优雅的一种方式是借助&lt;code&gt;蒲公英（pgyer）&lt;/code&gt;或&lt;code&gt;fir.im&lt;/code&gt;等平台，将&lt;code&gt;ipa/apk&lt;/code&gt;文件上传至平台后由平台生成二维码，然后只需要对二维码链接进行分发，体验用户通过手机扫描二维码后即可实现快速安装，效率得到了极大的提升。&lt;/p&gt;
&lt;h3 id="上传安装包文件，生成二维码"&gt;上传安装包文件，生成二维码&lt;/h3&gt;
&lt;p&gt;不管是&lt;code&gt;蒲公英&lt;/code&gt;还是&lt;code&gt;fir.im&lt;/code&gt;，都有对应的 Jenkins 插件，安装插件后可以在&lt;code&gt;Post-build&lt;/code&gt;中实现对安装包的上传。&lt;/p&gt;

&lt;p&gt;除了使用 Jenkins 插件，&lt;code&gt;fir.im&lt;/code&gt;还支持命令上传的方式，&lt;code&gt;蒲公英&lt;/code&gt;还支持&lt;code&gt;HTTP Post&lt;/code&gt;接口上传的方式。&lt;/p&gt;

&lt;p&gt;我个人推荐采用命令或接口上传的方法，并在构建脚本中进行调用。灵活是一方面，更大的好处是如果上传失败后还能进行重试，这在网络环境不是很稳定的情况下极其必要。&lt;/p&gt;

&lt;p&gt;Jenkins 成功完成安装包上传后，&lt;code&gt;pgyer/fir.im&lt;/code&gt;平台会生成一个二维码图片，并在响应中将图片的 URL 链接地址进行返回。&lt;/p&gt;
&lt;h3 id="展示二维码图片"&gt;展示二维码图片&lt;/h3&gt;
&lt;p&gt;二维码图片的 URL 链接有了，那要怎样才能将二维码图片展示在 Jenkins 项目的历史构建列表中呢？&lt;/p&gt;

&lt;p&gt;这里需要用到另外一个插件，&lt;code&gt;description setter plugin&lt;/code&gt;。安装该插件后，在&lt;code&gt;【Post-build Actions】&lt;/code&gt;栏目中会多出&lt;code&gt;description setter&lt;/code&gt;功能，可以实现构建完成后设置当次 build 的描述信息。这个描述信息不仅会显示在 build 页面中，同时也会显示在历史构建列表中。&lt;/p&gt;

&lt;p&gt;有了这个前提，要将二维码图片展示在历史构建列表中貌似就可以实现了，能直观想到的方式就是采用&lt;code&gt;HTML&lt;/code&gt;的&lt;code&gt;img&lt;/code&gt;标签，将&lt;code&gt;&amp;lt;img url='qr_code_url'&amp;gt;&lt;/code&gt;写入到 build 描述信息中。&lt;/p&gt;

&lt;p&gt;这个方法的思路是正确的，不过这么做以后并不会实现我们预期的效果。&lt;/p&gt;

&lt;p&gt;这是因为 Jenkins 出于安全的考虑，所有描述信息的&lt;code&gt;Markup Formatter&lt;/code&gt;默认都是采用&lt;code&gt;Plain text&lt;/code&gt;模式，在这种模式下是不会对 build 描述信息中的 HTML 编码进行解析的。&lt;/p&gt;

&lt;p&gt;要改变也很容易，&lt;code&gt;Manage Jenkins&lt;/code&gt; -&amp;gt; &lt;code&gt;Configure Global Security&lt;/code&gt;，将&lt;code&gt;Markup Formatter&lt;/code&gt;的设置更改为&lt;code&gt;Safe HTML&lt;/code&gt;即可。&lt;/p&gt;

&lt;p&gt;更改配置后，我们就可以在 build 描述信息中采用&lt;code&gt;HTML&lt;/code&gt;的&lt;code&gt;img&lt;/code&gt;标签插入图片了。&lt;/p&gt;

&lt;p&gt;另外还需要补充一个点。如果是使用&lt;code&gt;蒲公英（pyger）&lt;/code&gt;平台，会发现每次上传安装包后返回的二维码图片是一个短链接，神奇的是这个短连接居然是固定的（对同一个账号而言）。这个短连接总是指向最近生成的二维码图片，但是对于二维码图片的唯一 URL 地址，平台并没有在响应中进行返回。在这种情况下，我们每次构建完成后保存二维码图片的 URL 链接就没有意义了。&lt;/p&gt;

&lt;p&gt;应对的做法是，每次上传完安装包后，通过返回的二维码图片短链接将二维码图片下载并保存到本地，然后在 build 描述信息中引用该图片的 Jenkins 地址即可。&lt;/p&gt;
&lt;h3 id="收集编译成果物（Artifacts）"&gt;收集编译成果物（Artifacts）&lt;/h3&gt;
&lt;p&gt;每次完成构建后，编译生成的文件较多，但是并不是所有的文件都是我们需要的。&lt;/p&gt;

&lt;p&gt;通常情况下，我们可能只需要其中的部分文件，例如&lt;code&gt;.ipa/.app/.plist/.apk&lt;/code&gt;等，这时我们可以将这部分文件单独收集起来，并在构建页面中展示出来，以便在需要时进行下载。&lt;/p&gt;

&lt;p&gt;要实现这样一个功能，需要在&lt;code&gt;【Post-build Actions】&lt;/code&gt;栏目中新增&lt;code&gt;Archive the artifacts&lt;/code&gt;，然后在&lt;code&gt;Files to archive&lt;/code&gt;中通过正则表达式指定成果物文件的路径。&lt;/p&gt;

&lt;p&gt;设置完毕后，每次构建完成后，Jenkins 会在&lt;code&gt;Console Output&lt;/code&gt;中采用设定的正则表达式进行搜索匹配，如果能成功匹配到文件，则会将文件收集起来。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;本文主要是对如何使用 Jenkins 搭建 iOS/Android 持续集成打包平台的基础概念和实施流程进行了介绍。对于其中涉及到的执行命令、构建脚本（build.py），以及 Jenkins 的详细配置，出于篇幅长度和阅读体验的考虑，并没有在文中进行详细展开。&lt;/p&gt;

&lt;p&gt;为了实现真正的&lt;code&gt;开箱即用&lt;/code&gt;，我将 Jenkins 的配置文件和构建脚本抽离出来形成一套模板，只需要导入到 Jenkins 中，然后针对具体的项目修改少量配置信息，即可将这一套持续集成打包平台运行起来，实现和文章开头插图中完全相同的功能效果。&lt;/p&gt;

&lt;p&gt;详细内容请阅读《关于持续集成打包平台的 Jenkins 配置和构建脚本实现细节》。&lt;/p&gt;
&lt;h2 id="Read More ..."&gt;Read More ...&lt;/h2&gt;
&lt;p&gt;公众号：&lt;a href="http://debugtalk.com/assets/images/wechat_qrcode.png" rel="nofollow" target="_blank" title=""&gt;DebugTalk&lt;/a&gt;
原文链接：&lt;a href="http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins&lt;/a&gt;
GitHub 地址：&lt;a href="https://github.com/debugtalk/JenkinsTemplateForApp" rel="nofollow" target="_blank"&gt;https://github.com/debugtalk/JenkinsTemplateForApp&lt;/a&gt;&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Tue, 28 Jun 2016 16:50:10 +0800</pubDate>
      <link>https://ruby-china.org/topics/30391</link>
      <guid>https://ruby-china.org/topics/30391</guid>
    </item>
    <item>
      <title>在 Jenkins 中安装什么插件能实现这个效果呢？</title>
      <description>&lt;p&gt;请问下，在 Jenkins 中安装什么插件能实现这个效果呢？&lt;/p&gt;

&lt;p&gt;我试过&lt;a href="https://wiki.jenkins-ci.org/display/JENKINS/Build+Name+Setter+Plugin" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;Build Name Setter Plugin&lt;/code&gt;&lt;/a&gt;，通过 FILE 的形式设置 Build Name 后会产生乱码。&lt;/p&gt;

&lt;p&gt;还请了解这个的多多指教。&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Tue, 21 Jun 2016 21:36:45 +0800</pubDate>
      <link>https://ruby-china.org/topics/30342</link>
      <guid>https://ruby-china.org/topics/30342</guid>
    </item>
    <item>
      <title>深入浅出 Git 权限校验 (最熟悉的陌生概念)</title>
      <description>&lt;p&gt;借助上次&lt;a href="http://debugtalk.com/post/trap-in-GitHub-authority-verification" rel="nofollow" target="_blank" title=""&gt;“掉坑”的经历&lt;/a&gt;，我对 Git 权限校验的两种方式重头进行了梳理，形成了这篇总结记录。&lt;/p&gt;

&lt;p&gt;在本地计算机与 GitHub（或 GitLab）进行通信时，传输主要基于两种协议，&lt;code&gt;HTTPS&lt;/code&gt;和&lt;code&gt;SSH&lt;/code&gt;，对应的仓库地址就是&lt;code&gt;HTTPS URLs&lt;/code&gt;和&lt;code&gt;SSH URLs&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;首先需要强调的是，&lt;code&gt;HTTPS URLs&lt;/code&gt;和&lt;code&gt;SSH URLs&lt;/code&gt;对应的是两套完全独立的权限校验方式，主要的区别就是&lt;code&gt;HTTPS URLs&lt;/code&gt;采用账号密码进行校验，&lt;code&gt;SSH URLs&lt;/code&gt;采用&lt;code&gt;SSH&lt;/code&gt;秘钥对进行校验。平时使用的时候我们可以根据实际情况，选择一种即可。&lt;/p&gt;
&lt;h2 id="HTTPS URLs"&gt;HTTPS URLs&lt;/h2&gt;
&lt;p&gt;GitHub 官方推荐采用&lt;code&gt;HTTPS URLs&lt;/code&gt;的方式，因为该种方式适用面更广（即使在有防火墙或代理的情况下也同样适用），使用更方便（配置更简单）。&lt;/p&gt;

&lt;p&gt;采用&lt;code&gt;HTTPS URLs&lt;/code&gt;地址&lt;code&gt;clone&lt;/code&gt;/&lt;code&gt;fetch&lt;/code&gt;/&lt;code&gt;pull&lt;/code&gt;/&lt;code&gt;push&lt;/code&gt;仓库时，事先无需对本地系统进行任何配置，只需要输入 GitHub 的账号和密码即可。不过如果每次都要手动输入账号密码，也是一件很繁琐的事情。&lt;/p&gt;

&lt;p&gt;好在已经有多个机制可以让操作不用这么麻烦。&lt;/p&gt;

&lt;p&gt;在 Mac 系统中，在启用&lt;code&gt;Keychain&lt;/code&gt;机制的情况下，首次输入 GitHub 账号密码后，认证信息就会自动保存到系统的&lt;code&gt;Keychain&lt;/code&gt;中，下次再次访问仓库时就会自动读取&lt;code&gt;Keychain&lt;/code&gt;中保存的认证信息。&lt;/p&gt;

&lt;p&gt;在非 Mac 系统中，虽然没有&lt;code&gt;Keychain&lt;/code&gt;机制，但是 Git 提供了&lt;code&gt;credential helper&lt;/code&gt;机制，可以将账号密码以 cache 的形式在内存中缓存一段时间（默认 15 分钟），或者以文件的形式存储起来（&lt;code&gt;~/.git-credentials&lt;/code&gt;）。当然，Mac 系统如果不启用&lt;code&gt;Keychain&lt;/code&gt;机制，也可以采用这种方式。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# cache credential in memory&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; credential.helper cache
&lt;span class="c"&gt;# store credential in ~/.git-credential&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; credential.helper store
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;credential.helper&lt;/code&gt;设置为&lt;code&gt;store&lt;/code&gt;的情况下，首次输入 GitHub 账号密码后，就会自动保存到&lt;code&gt;~/.git-credentials&lt;/code&gt;文件中，保存形式为&lt;code&gt;https://user:pass@github.com&lt;/code&gt;；下次再次访问仓库时就会自动读取&lt;code&gt;~/.git-credentials&lt;/code&gt;中保存的认证信息。&lt;/p&gt;

&lt;p&gt;另一个需要说明的情况是，如果在 GitHub 中开启了&lt;code&gt;2FA（two-factor authentication）&lt;/code&gt;，那么在本地系统中输入 GitHub 账号密码时，不能输入原始的密码（即 GitHub 网站的登录密码），而是需要事先在 GitHub 网站中创建一个&lt;code&gt;Personal access token&lt;/code&gt;，后续在访问代码仓库需要进行权限校验的时候，采用&lt;code&gt;access token&lt;/code&gt;作为密码进行输入。&lt;/p&gt;
&lt;h2 id="SSH URLs"&gt;SSH URLs&lt;/h2&gt;
&lt;p&gt;除了&lt;code&gt;HTTPS URLs&lt;/code&gt;，还可以采用&lt;code&gt;SSH URLs&lt;/code&gt;的方式访问 GitHub 代码仓库。&lt;/p&gt;

&lt;p&gt;采用&lt;code&gt;SSH URLs&lt;/code&gt;方式之前，需要先在本地计算机中生成&lt;code&gt;SSH keypair&lt;/code&gt;（秘钥对，包括私钥和公钥）。默认情况下，生成的秘钥位于&lt;code&gt;$HOME/.ssh/&lt;/code&gt;目录中，文件名称分别为&lt;code&gt;id_rsa&lt;/code&gt;和&lt;code&gt;id_rsa.pub&lt;/code&gt;，通常无需修改，保持默认即可。不过，如果一台计算机中存在多个秘钥对，就需要修改秘钥文件名，名称没有强制的命名规范，便于自己辨识即可。&lt;/p&gt;

&lt;p&gt;如下是创建秘钥对的过程。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; rsa &lt;span class="nt"&gt;-b&lt;/span&gt; 4096 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"mail@debugtalk.com"&lt;/span&gt;
Generating public/private rsa key pair.
Enter file &lt;span class="k"&gt;in &lt;/span&gt;which to save the key &lt;span class="o"&gt;(&lt;/span&gt;/Users/Leo/.ssh/id_rsa&lt;span class="o"&gt;)&lt;/span&gt;: /Users/Leo/.ssh/debugtalk_id_rsa
Enter passphrase &lt;span class="o"&gt;(&lt;/span&gt;empty &lt;span class="k"&gt;for &lt;/span&gt;no passphrase&lt;span class="o"&gt;)&lt;/span&gt;: &amp;lt;myPassphrase&amp;gt;
Enter same passphrase again: &amp;lt;myPassphrase&amp;gt;
Your identification has been saved &lt;span class="k"&gt;in&lt;/span&gt; /Users/Leo/.ssh/debugtalk_id_rsa.
Your public key has been saved &lt;span class="k"&gt;in&lt;/span&gt; /Users/Leo/.ssh/debugtalk_id_rsa.pub.
The key fingerprint is:
SHA256:jCyEEKjlCU1klROnuBg+UH08GJ1u252rQMADdD9kYMo mail@debugtalk.com
The key&lt;span class="s1"&gt;'s randomart image is:
+---[RSA 4096]----+
|+*BoBO+.         |
|o=oO=**          |
|++E.*+o.         |
|+ooo +o+         |
|.o. ..+oS. .     |
|  .  o. . o      |
|      .    .     |
|       .  .      |
|        ..       |
+----[SHA256]-----+
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在创建秘钥的过程中，系统还建议创建一个名为&lt;code&gt;passphrase&lt;/code&gt;的东西，这是用来干嘛的呢？&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;首先，单独采用密码肯定是不够安全的。如果密码太简单，那么就很容易被暴力破解，如果密码太复杂，那么用户就很难记忆，记录到小本子里面更不安全。&lt;/p&gt;

&lt;p&gt;因此，&lt;code&gt;SSH keys&lt;/code&gt;诞生了。&lt;code&gt;SSH&lt;/code&gt;秘钥对的可靠性非常高，被暴力破解的可能性基本没有。不过，这要求用户非常谨慎地保管好私钥，如果别人使用你的计算机时偷偷地将你的私钥拷走了，那么就好比是别人拿到了你家里的钥匙，也能随时打开你家的门。&lt;/p&gt;

&lt;p&gt;基于以上情况，解决办法就是在&lt;code&gt;SSH keys&lt;/code&gt;之外再增加一个密码，即&lt;code&gt;passphrase&lt;/code&gt;。只有同时具备&lt;code&gt;SSH private key&lt;/code&gt;和&lt;code&gt;passphrase&lt;/code&gt;的情况下，才能通过&lt;code&gt;SSH&lt;/code&gt;的权限校验，这就大大地增加了安全性。当然，这个&lt;code&gt;passphrase&lt;/code&gt;也不是必须的，在创建秘钥对时也可以不设置&lt;code&gt;passphrase&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;另外，如果每次权限校验时都要输入&lt;code&gt;passphrase&lt;/code&gt;，这也是挺麻烦的。好在我们不用再担心这个问题，因为&lt;code&gt;ssh-agent&lt;/code&gt;可以帮我们记住&lt;code&gt;passphrase&lt;/code&gt;，Mac 系统的 Keychain 也可以记住&lt;code&gt;passphrase&lt;/code&gt;，这样我们在同一台计算机中就不用重新输入密码了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;秘钥对创建好以后，私钥存放于本地计算机（&lt;code&gt;~/.ssh/id_rsa&lt;/code&gt;），将公钥（&lt;code&gt;~/.ssh/id_rsa.pub&lt;/code&gt;）中的内容添加至 GitHub 账户。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# copy the contents of id_rsa.pub to the clipboard&lt;/span&gt;
➜ pbcopy &amp;lt; ~/.ssh/id_rsa.pub

&lt;span class="c"&gt;# paste to GitHub&lt;/span&gt;
&lt;span class="c"&gt;# Login GitHub, 【Settings】-&amp;gt;【SSH and GPG keys】-&amp;gt;【New SSH Key】&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过，如果此时检测本地计算机与 GitHub 的连接状态，会发现系统仍提示权限校验失败。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ ssh &lt;span class="nt"&gt;-T&lt;/span&gt; git@github.com 
Permission denied &lt;span class="o"&gt;(&lt;/span&gt;publickey&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是因为在本地计算机与 GitHub 建立连接的时候，实际上是本机计算机的&lt;code&gt;ssh-agent&lt;/code&gt;与 GitHub 服务器进行通信。虽然本地计算机有了私钥，但是&lt;code&gt;ssh-agent&lt;/code&gt;并不知道私钥存储在哪儿。因此，要想正常使用秘钥对，需要先将私钥加入到本地计算机的&lt;code&gt;ssh-agent&lt;/code&gt;中（添加过程中需要输入&lt;code&gt;passphrase&lt;/code&gt;）。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# start ssh-agent in the background&lt;/span&gt;
➜ &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ssh-agent &lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
Agent pid 78370

➜ ssh-add ~/.ssh/id_rsa
Enter passphrase &lt;span class="k"&gt;for&lt;/span&gt; /Users/Leo/.ssh/id_rsa: &amp;lt;myPassphrase&amp;gt;
Identity added: /Users/Leo/.ssh/id_rsa &lt;span class="o"&gt;(&lt;/span&gt;/Users/Leo/.ssh/id_rsa&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加完成后，就可以查看到当前计算机中存储的密钥。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt;                
4096 SHA256:xRg49AgTxxxxxxxx8q2SPPOfxxxxxxxxRlBY /Users/Leo/.ssh/id_rsa &lt;span class="o"&gt;(&lt;/span&gt;RSA&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次检测本地计算机与 GitHub 的连接状态，校验就正常通过了。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ ssh &lt;span class="nt"&gt;-T&lt;/span&gt; git@github.com
Hi djileolee! You&lt;span class="s1"&gt;'ve successfully authenticated, but GitHub does not provide shell access.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后续再进行&lt;code&gt;clone&lt;/code&gt;/&lt;code&gt;fetch&lt;/code&gt;/&lt;code&gt;pull&lt;/code&gt;/&lt;code&gt;push&lt;/code&gt;操作时，就可以正常访问 GitHub 代码仓库了，并且也不需要再重新输入账号密码。&lt;/p&gt;

&lt;p&gt;而且，将私钥加入&lt;code&gt;ssh-agent&lt;/code&gt;后，即使删除私钥文件，本地计算机仍可以正常访问 GitHub 代码仓库。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ~/.ssh
➜ ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt;    
4096 SHA256:xRg49AgTxxxxxxxx8q2SPPOfxxxxxxxxRlBY /Users/Leo/.ssh/id_rsa &lt;span class="o"&gt;(&lt;/span&gt;RSA&lt;span class="o"&gt;)&lt;/span&gt;
➜ ssh &lt;span class="nt"&gt;-T&lt;/span&gt; git@github.com
The authenticity of host &lt;span class="s1"&gt;'github.com (192.30.252.130)'&lt;/span&gt; can&lt;span class="s1"&gt;'t be established.
RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '&lt;/span&gt;github.com,192.30.252.130&lt;span class="s1"&gt;' (RSA) to the list of known hosts.
Hi djileolee! You'&lt;/span&gt;ve successfully authenticated, but GitHub does not provide shell access.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有执行&lt;code&gt;ssh-add -D&lt;/code&gt;或&lt;code&gt;ssh-add -d pub_key&lt;/code&gt;命令，将私钥从&lt;code&gt;ssh-agent&lt;/code&gt;删除后，认证信息才会失效。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ ssh-add &lt;span class="nt"&gt;-d&lt;/span&gt; ~/.ssh/id_rsa.pub
Identity removed: /Users/Leo/.ssh/id_rsa.pub &lt;span class="o"&gt;(&lt;/span&gt;mail@debugtalk.com&lt;span class="o"&gt;)&lt;/span&gt;
➜ ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt;
The agent has no identities.
➜ ssh &lt;span class="nt"&gt;-T&lt;/span&gt; git@github.com
Permission denied &lt;span class="o"&gt;(&lt;/span&gt;publickey&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="同时使用多个GitHub账号"&gt;同时使用多个 GitHub 账号&lt;/h2&gt;
&lt;p&gt;熟悉了&lt;code&gt;HTTPS URLs&lt;/code&gt;和&lt;code&gt;SSH URLs&lt;/code&gt;这两种校验方式之后，我们再来看之前遇到的问题。要想在一台计算机上同时使用多个 GitHub 账号访问不同的仓库，需要怎么做呢？&lt;/p&gt;

&lt;p&gt;为了更好地演示，现假设有两个 GitHub 账号，&lt;code&gt;debugtalk&lt;/code&gt;和&lt;code&gt;djileolee&lt;/code&gt;，在两个账号中各自有一个仓库，&lt;code&gt;debugtalk/DroidMeter&lt;/code&gt;和&lt;code&gt;DJIXY/MobileStore&lt;/code&gt;（公司私有库）。&lt;/p&gt;

&lt;p&gt;前面已经说过，&lt;code&gt;HTTPS URLs&lt;/code&gt;和&lt;code&gt;SSH URLs&lt;/code&gt;对应着两套独立的权限校验方式，因此这两套方式应该是都能单独实现我们的需求的。&lt;/p&gt;

&lt;p&gt;不过在详细讲解 Git 权限校验的问题之前，我们先来回顾下 Git 配置文件的优先级。&lt;/p&gt;
&lt;h3 id="Git配置存储位置及其优先级"&gt;Git 配置存储位置及其优先级&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Unix-like&lt;/code&gt;系统中，保存 Git 用户信息的主要有 3 个地方（Mac 系统多一个&lt;code&gt;Keychain&lt;/code&gt;）：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/etc/gitconfig&lt;/code&gt;：存储当前系统所有用户的 git 配置信息，使用带有&lt;code&gt;--system&lt;/code&gt;选项的&lt;code&gt;git config&lt;/code&gt;时，配置信息会写入该文件；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.gitconfig&lt;/code&gt;或&lt;code&gt;~/.config/git/config&lt;/code&gt;：存储当前用户的 git 配置信息，使用带有&lt;code&gt;--global&lt;/code&gt;选项的&lt;code&gt;git config&lt;/code&gt;时，配置信息会写入该文件；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Keychain Access&lt;/code&gt;：在开启&lt;code&gt;Keychain&lt;/code&gt;机制的情况下，进行权限校验后会自动将账号密码保存至&lt;code&gt;Keychain Access&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;仓库的 Git 目录中的 config 文件（即&lt;code&gt;repo/.git/config&lt;/code&gt;）：存储当前仓库的 git 配置信息，在仓库中使用带有&lt;code&gt;--local&lt;/code&gt;选项的&lt;code&gt;git config&lt;/code&gt;时，配置信息会写入该文件；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在优先级方面，以上 4 个配置项的优先级从上往下依次上升，即&lt;code&gt;repo/.git/config&lt;/code&gt;的优先级最高，然后&lt;code&gt;Keychain Access&lt;/code&gt;会覆盖&lt;code&gt;~/.gitconfig&lt;/code&gt;中的配置，&lt;code&gt;~/.gitconfig&lt;/code&gt;会覆盖&lt;code&gt;/etc/gitconfig&lt;/code&gt;中的配置。&lt;/p&gt;
&lt;h3 id="基于SSH协议实现多账号共存"&gt;基于&lt;code&gt;SSH&lt;/code&gt;协议实现多账号共存&lt;/h3&gt;
&lt;p&gt;先来看下如何采用&lt;code&gt;SSH URLs&lt;/code&gt;实现我们的需求。&lt;/p&gt;

&lt;p&gt;在处理多账号共存问题之前，两个账号均已分别创建&lt;code&gt;SSH&lt;/code&gt;秘钥对，并且&lt;code&gt;SSH-key&lt;/code&gt;均已加入本地计算机的&lt;code&gt;ssh-agent&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt;
4096 SHA256:lqujbjkWM1xxxxxxxxxxG6ERK6DNYj9tXExxxxxx8ew /Users/Leo/.ssh/dji_id_rsa &lt;span class="o"&gt;(&lt;/span&gt;RSA&lt;span class="o"&gt;)&lt;/span&gt;
4096 SHA256:II2O9vZutdQr8xxxxxxxxxxD7EYvxxxxxxbynx2hHtg /Users/Leo/.ssh/id_rsa &lt;span class="o"&gt;(&lt;/span&gt;RSA&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在详细讲解多账号共存的问题之前，我们先来回想下平时在 Terminal 中与 GitHub 仓库进行交互的场景。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  DroidMeter git:&lt;span class="o"&gt;(&lt;/span&gt;master&lt;span class="o"&gt;)&lt;/span&gt; git pull
Already up-to-date.
➜  DroidMeter git:&lt;span class="o"&gt;(&lt;/span&gt;master&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;touch &lt;/span&gt;README.md
➜  DroidMeter git:&lt;span class="o"&gt;(&lt;/span&gt;master&lt;span class="o"&gt;)&lt;/span&gt; ✗ git add &lt;span class="nb"&gt;.&lt;/span&gt;
➜  DroidMeter git:&lt;span class="o"&gt;(&lt;/span&gt;master&lt;span class="o"&gt;)&lt;/span&gt; ✗ git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"add README"&lt;/span&gt;
➜  DroidMeter git:&lt;span class="o"&gt;(&lt;/span&gt;master&lt;span class="o"&gt;)&lt;/span&gt; git push
Counting objects: 3, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Delta compression using up to 4 threads.
Compressing objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;2/2&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Writing objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;3/3&lt;span class="o"&gt;)&lt;/span&gt;, 310 bytes | 0 bytes/s, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Total 3 &lt;span class="o"&gt;(&lt;/span&gt;delta 0&lt;span class="o"&gt;)&lt;/span&gt;, reused 0 &lt;span class="o"&gt;(&lt;/span&gt;delta 0&lt;span class="o"&gt;)&lt;/span&gt;
To git@debugtalk:debugtalk/DroidMeter.git
   7df6839..68d085b  master -&amp;gt; master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在操作过程中，本地计算机的&lt;code&gt;ssh-agent&lt;/code&gt;与 GitHub 服务器建立了连接，并进行了账号权限校验。&lt;/p&gt;

&lt;p&gt;当本地计算机只有一个 GitHub 账号时，这个行为并不难理解，系统应该会采用这个唯一的 GitHub 账号进行操作。那如果本地计算机中有多个 Github 账号时，系统是根据什么来判断应该选择哪个账号呢？&lt;/p&gt;

&lt;p&gt;实际情况是，系统没法进行判断。系统只会有一个默认的账号，然后采用这个默认的账号去操作所有的代码仓库，当账号与仓库不匹配时，就会报权限校验失败的错误。&lt;/p&gt;

&lt;p&gt;那要怎样才能让系统正确区分账号呢？这就需要我们手动进行配置，配置文件即是&lt;code&gt;~/.ssh/config&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;创建&lt;code&gt;~/.ssh/config&lt;/code&gt;文件，在其中填写如下内容。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# debugtalk&lt;/span&gt;
Host debugtalk
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_rsa

&lt;span class="c"&gt;# DJI&lt;/span&gt;
Host djileolee
    HostName github.com
    User git
    IdentityFile ~/.ssh/dji_id_rsa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要理解以上配置文件的含义并不难，我们可以对比看下两个项目的&lt;code&gt;SSH URLs&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git@github.com:debugtalk/DroidMeter.git
git@github.com:DJISZ/Store_Android.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;git&lt;/code&gt;是本地&lt;code&gt;ssh-agent&lt;/code&gt;与 GitHub 服务器建立&lt;code&gt;SSH&lt;/code&gt;连接采用的用户名（即&lt;code&gt;User&lt;/code&gt;），&lt;code&gt;github.com&lt;/code&gt;是 GitHub 服务器的主机（即&lt;code&gt;HostName&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;可以看出，如果采用原始的&lt;code&gt;SSH URLs&lt;/code&gt;，由于&lt;code&gt;User&lt;/code&gt;和&lt;code&gt;HostName&lt;/code&gt;都相同，本地计算机并不知道应该采用哪个&lt;code&gt;SSH-key&lt;/code&gt;去建立连接。&lt;/p&gt;

&lt;p&gt;因此，通过创建&lt;code&gt;~/.ssh/config&lt;/code&gt;文件，在&lt;code&gt;Host&lt;/code&gt;中进行区分，然后经过&lt;code&gt;CNAME&lt;/code&gt;映射到&lt;code&gt;HostName&lt;/code&gt;，然后分别指向不同的&lt;code&gt;SSH-key&lt;/code&gt;，即&lt;code&gt;IdentityFile&lt;/code&gt;。由于&lt;code&gt;HostName&lt;/code&gt;才是真正指定 GitHub 服务器主机的字段，因此这么配置不会对本地&lt;code&gt;ssh-agent&lt;/code&gt;连接 GitHub 主机产生影响，再加上&lt;code&gt;Host&lt;/code&gt;别名指向了不同的&lt;code&gt;SSH-key&lt;/code&gt;，从而实现了对两个 GitHub 账号的分离。&lt;/p&gt;

&lt;p&gt;配置完毕后，两个 GitHub 账号就可以通过&lt;code&gt;Host&lt;/code&gt;别名来进行区分了。后续再与 GitHub 服务器进行通信时，就可以采用&lt;code&gt;Host&lt;/code&gt;别名代替原先的&lt;code&gt;github.com&lt;/code&gt;。例如，测试本地&lt;code&gt;ssh-agent&lt;/code&gt;与 GitHub 服务器的连通性时，可采用如下方式：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ ssh &lt;span class="nt"&gt;-T&lt;/span&gt; git@debugtalk 
Hi debugtalk! You have successfully authenticated, but GitHub does not provide shell access.
➜ ssh &lt;span class="nt"&gt;-T&lt;/span&gt; git@djileolee
Hi djileolee! You have successfully authenticated, but GitHub does not provide shell access.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，此时两个账号各司其职，不会再出现混淆的情况。&lt;/p&gt;

&lt;p&gt;不过，我们还遗漏了很重要的一点。在本地代码仓库中执行&lt;code&gt;push&lt;/code&gt;/&lt;code&gt;pull&lt;/code&gt;/&lt;code&gt;fetch&lt;/code&gt;等操作的时候，命令中并不会包含&lt;code&gt;Host&lt;/code&gt;信息，那系统怎么知道我们要采用哪个 GitHub 账号进行操作呢？&lt;/p&gt;

&lt;p&gt;答案是，系统还是没法判断，需要我们进行配置指定。&lt;/p&gt;

&lt;p&gt;显然，不同的仓库可能对应着不同的 GitHub 账号，因此这个配置不能配置成全局的，而只能在各个项目中分别进行配置，即&lt;code&gt;repo/.git/config&lt;/code&gt;文件。&lt;/p&gt;

&lt;p&gt;配置的方式如下：&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;debugtalk/DroidMeter&lt;/code&gt;仓库中：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ git remote add origin git@debugtalk:debugtalk/DroidMeter.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;DJIXY/MobileStore.git&lt;/code&gt;仓库中：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ git remote add origin git@djileolee:DJIXY/MobileStore.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置的原理也很容易理解，就是将仓库的&lt;code&gt;Host&lt;/code&gt;更换为之前设置的别名。添加完毕后，后续再在两个仓库中执行任何&lt;code&gt;git&lt;/code&gt;操作时，系统就可以选择正确的&lt;code&gt;SSH-key&lt;/code&gt;与 GitHub 服务器进行交互了。&lt;/p&gt;
&lt;h3 id="基于HTTPS协议实现多账号共存"&gt;基于&lt;code&gt;HTTPS&lt;/code&gt;协议实现多账号共存&lt;/h3&gt;
&lt;p&gt;再来看下如何采用&lt;code&gt;HTTPS URLs&lt;/code&gt;实现我们的需求。&lt;/p&gt;

&lt;p&gt;有了前面的经验，我们的思路就清晰了许多。采用&lt;code&gt;HTTPS URLs&lt;/code&gt;的方式进行 Git 权限校验后，系统会将 GitHub 账号密码存储到&lt;code&gt;Keychain&lt;/code&gt;中（Mac 系统），或者存储到&lt;code&gt;~/.git-credentials&lt;/code&gt;文件中（&lt;code&gt;Git credential helper&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;不管是存储到哪里，我们面临的问题都是相同的，即如何在代码仓库中区分采用哪个 GitHub 账号。&lt;/p&gt;

&lt;p&gt;配置的方式其实也很简单：&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;debugtalk/DroidMeter&lt;/code&gt;仓库中：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ git remote add origin https://debugtalk@github.com/debugtalk/DroidMeter.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;DJIXY/MobileStore.git&lt;/code&gt;仓库中：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ git remote add origin https://djileolee@github.com/DJIXY/MobileStore.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置的原理也很容易理解，将 GitHub 用户名添加到仓库的 Git 地址中，这样在执行 git 命令的时候，系统就会采用指定的 GitHub 用户名去&lt;code&gt;Keychain&lt;/code&gt;或&lt;code&gt;~/.git-credentials&lt;/code&gt;中寻找对应的认证信息，账号使用错乱的问题也就不复存在了。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Done!&lt;/code&gt;&lt;/p&gt;
&lt;h2 id="Read More ..."&gt;Read More ...&lt;/h2&gt;
&lt;p&gt;公众号：&lt;a href="http://debugtalk.com/assets/images/wechat_qrcode.png" rel="nofollow" target="_blank" title=""&gt;DebugTalk&lt;/a&gt;
原文链接：&lt;a href="http://debugtalk.com/post/head-first-git-authority-verification" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/head-first-git-authority-verification&lt;/a&gt;&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Fri, 17 Jun 2016 09:48:59 +0800</pubDate>
      <link>https://ruby-china.org/topics/30298</link>
      <guid>https://ruby-china.org/topics/30298</guid>
    </item>
    <item>
      <title>GitHub 权限校验失败给我的启发</title>
      <description>&lt;h2 id="背景描述"&gt;背景描述&lt;/h2&gt;
&lt;p&gt;众所周知，在 GitHub 中，每个仓库都有两个地址，分别基于&lt;code&gt;HTTPS&lt;/code&gt;协议和&lt;code&gt;SSH&lt;/code&gt;协议，两个协议对应的 URL 地址（repository_url）形式如下所示：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# HTTPS&lt;/span&gt;
https://github.com/DJIXY/MobileStore.git
&lt;span class="c"&gt;# SSH&lt;/span&gt;
git@github.com:DJIXY/MobileStore.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常情况下，只要在本地正确地配置好了&lt;code&gt;git&lt;/code&gt;账号，采用这两个地址中的任意一个，都可以通过&lt;code&gt;git clone repository_url&lt;/code&gt;获取代码。&lt;/p&gt;

&lt;p&gt;但最近我在 Macbook Air 中&lt;code&gt;clone&lt;/code&gt;公司托管在 GitHub 私有库中的代码时，发现无法通过&lt;code&gt;HTTPS&lt;/code&gt;协议的地址&lt;code&gt;clone&lt;/code&gt;代码，始终提示&lt;code&gt;remote: Repository not found.&lt;/code&gt;的错误。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ git clone https://github.com/DJIXY/MobileStore.git
Cloning into &lt;span class="s1"&gt;'MobileStore'&lt;/span&gt;...
remote: Repository not found.
fatal: repository &lt;span class="s1"&gt;'https://github.com/DJIXY/MobileStore.git/'&lt;/span&gt; not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，这个代码仓库是确实存在的，而且地址肯定也是没有问题的，通过 URL 地址也能在浏览器中访问到对应的 GitHub 仓库页面。&lt;/p&gt;

&lt;p&gt;其次，在本地对&lt;code&gt;git&lt;/code&gt;的配置也是没有问题的，通过&lt;code&gt;SSH&lt;/code&gt;协议的地址是可以正常&lt;code&gt;clone&lt;/code&gt;代码的。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ git clone git@github.com:DJIXY/MobileStore.git
Cloning into &lt;span class="s1"&gt;'MobileStore'&lt;/span&gt;...
Warning: Permanently added the RSA host key &lt;span class="k"&gt;for &lt;/span&gt;IP address &lt;span class="s1"&gt;'192.30.252.131'&lt;/span&gt; to the list of known hosts.
remote: Counting objects: 355, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Compressing objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;3/3&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且，如果在&lt;code&gt;HTTPS&lt;/code&gt;协议的 URL 地址中加上 GitHub 账号，也是可以正常&lt;code&gt;clone&lt;/code&gt;代码的。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ git clone https://djileolee@github.com/DJIXY/MobileStore.git
Cloning into &lt;span class="s1"&gt;'MobileStore'&lt;/span&gt;...
remote: Counting objects: 355, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Compressing objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;3/3&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更奇怪的是，在我的另一台 Mac Mini 中，采用同样的账号配置，两种协议的 URL 地址却都能正常&lt;code&gt;clone&lt;/code&gt;代码，仔细地对比了两台电脑的&lt;code&gt;git&lt;/code&gt;配置，都是一样的。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ &lt;span class="nb"&gt;cat&lt;/span&gt; ~/.git-credentials
https://djileolee:340d247cxxxxxxxxf39556e38fe2b0baxxxxxxxx@github.com
➜
➜ &lt;span class="nb"&gt;cat&lt;/span&gt; ~/.gitconfig
&lt;span class="o"&gt;[&lt;/span&gt;credential]
    helper &lt;span class="o"&gt;=&lt;/span&gt; store
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那问题出在哪儿呢？&lt;/p&gt;
&lt;h2 id="定位分析"&gt;定位分析&lt;/h2&gt;
&lt;p&gt;通过 Google 得知，产生&lt;code&gt;remote: Repository not found.&lt;/code&gt;报错的原因主要有两个，一是仓库地址错误，二是权限校验不通过。显然，第一个原因可以直接排除，在 Macbook Air 中出现该问题应该就是账号权限校验失败造成的。&lt;/p&gt;

&lt;p&gt;对背景描述中的现象进行整理，重点关注两个疑点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;通过&lt;code&gt;HTTPS&lt;/code&gt;协议的 URL 地址进行&lt;code&gt;git clone&lt;/code&gt;时，系统没有提示让输入用户名密码，就直接返回权限校验失败的异常；&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;HTTPS&lt;/code&gt;协议的 URL 地址中加上 GitHub 用户名，就可以正常&lt;code&gt;clone&lt;/code&gt;，而且，系统也没有提示输入密码。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这说明，在系统中的某个地方，应该是保存了 GitHub 账号密码的，所以在未指定账号的情况下，&lt;code&gt;git clone&lt;/code&gt;时系统就不再要求用户输入账号密码，而是直接读取那个保存好的账号信息；但是，那个保存的 GitHub 账号密码应该是存在问题的，这就造成采用那个账号信息去 GitHub 校验时无法通过，从而返回异常报错。&lt;/p&gt;

&lt;p&gt;基于以上推测，寻找问题根源的当务之急是找到保存 GitHub 账号密码的地方。&lt;/p&gt;

&lt;p&gt;通过查看 Git 官方文档，存储 Git 用户信息的地方有三个：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/etc/gitconfig&lt;/code&gt;：存储当前系统所有用户的 git 配置信息；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.gitconfig&lt;/code&gt;或&lt;code&gt;~/.config/git/config&lt;/code&gt;：存储当前用户的 git 配置信息；&lt;/li&gt;
&lt;li&gt;仓库的 Git 目录中的 config 文件（即&lt;code&gt;repo/.git/config&lt;/code&gt;）：存储当前仓库的 git 配置信息。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这三个配置项的优先级从上往下依次上升，即&lt;code&gt;repo/.git/config&lt;/code&gt;会覆盖&lt;code&gt;~/.gitconfig&lt;/code&gt;中的配置，&lt;code&gt;~/.gitconfig&lt;/code&gt;会覆盖&lt;code&gt;/etc/gitconfig&lt;/code&gt;中的配置。&lt;/p&gt;

&lt;p&gt;回到当前问题，由于还没有进入到具体的 Git 仓库，因此&lt;code&gt;repo/.git/config&lt;/code&gt;可直接排除；然后是查看当前用户的 git 配置，在当前用户 HOME 目录下没有&lt;code&gt;~/.config/git/config&lt;/code&gt;文件，只有&lt;code&gt;~/.gitconfig&lt;/code&gt;，不过在&lt;code&gt;~/.gitconfig&lt;/code&gt;中并没有账号信息；再去查看系统级的 git 配置信息，即&lt;code&gt;/etc/gitconfig&lt;/code&gt;文件，但发现当前系统中并没有该文件。&lt;/p&gt;

&lt;p&gt;找遍了 Git 用户信息可能存储的地方，都没有看到账号配置信息，那还可能存储在哪儿呢？&lt;/p&gt;

&lt;p&gt;这时基本上是毫无思路了，只能靠各种胡乱猜测，甚至尝试采用 Wireshark 分别在两台 Mac 上对&lt;code&gt;git clone&lt;/code&gt;的过程进行抓包，对比通讯数据的差异，但都没有找到答案。&lt;/p&gt;

&lt;p&gt;最后，无意中想到了 Mac 的&lt;code&gt;Keychain&lt;/code&gt;机制。在 Mac OSX 的&lt;code&gt;Keychain&lt;/code&gt;中，可以保存用户的账号密码等&lt;code&gt;credentials&lt;/code&gt;，那 git 账号会不会也保存到&lt;code&gt;Keychain&lt;/code&gt;中了呢？&lt;/p&gt;

&lt;p&gt;在 Macbook Air 中打开&lt;code&gt;Keychain Access&lt;/code&gt;应用软件，搜索&lt;code&gt;github&lt;/code&gt;，果然发现存在记录。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/Mac_Keychain_GitHub.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;而且，&lt;code&gt;github.com&lt;/code&gt;这一项还存在两条记录。一条是我的个人账号&lt;code&gt;debugtalk&lt;/code&gt;，另一条是公司的工作账号&lt;code&gt;djileolee&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;至此，真相大白！！！&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在我的 Macbook Air 中，&lt;code&gt;Keychain Access&lt;/code&gt;中保存了我的 GitHub 个人账号（&lt;code&gt;debugtalk&lt;/code&gt;），该账号是没有权限访问公司私有仓库的。但是在 Terminal 中执行&lt;code&gt;git clone&lt;/code&gt;命令时，系统优先读取了我的个人账号，并用该账号向 GitHub 发起校验请求，从而造成读取公司私有仓库时权限校验失败。然而，在&lt;code&gt;HTTPS&lt;/code&gt;协议的 URL 地址中加上 GitHub 工作账号（&lt;code&gt;djileolee&lt;/code&gt;）时，由于此时指定了账号名称，因此在&lt;code&gt;Keychain&lt;/code&gt;中读取账号信息时就可以找到对应账号（包含密码），并且在无需输入密码的情况下就能成功通过 GitHub 的权限校验，进而成功&lt;code&gt;clone&lt;/code&gt;得到代码。&lt;/p&gt;

&lt;p&gt;原因弄清楚之后，解决方式就很简单了，在&lt;code&gt;Keychain&lt;/code&gt;中删除个人账号，然后就正常了。&lt;/p&gt;
&lt;h2 id="总结回顾"&gt;总结回顾&lt;/h2&gt;
&lt;p&gt;但是，问题真的解决了么？&lt;/p&gt;

&lt;p&gt;并没有！&lt;/p&gt;

&lt;p&gt;简单粗暴地在 Keychain 中将个人 GitHub 账号删除了，虽然再次访问公司代码仓库时正常了，那我要再访问个人仓库时该怎么办呢？&lt;/p&gt;

&lt;p&gt;貌似并没有清晰的思路。虽然网上也有不少操作指导教程，但是对于操作背后的原理，还是有很多不清晰的地方。&lt;/p&gt;

&lt;p&gt;再回到前面的背景描述，以及定位问题的整个过程，不由地悲从中来。使用 GitHub 好歹也有好几年了，但是连最基本的概念都还一头雾水，所以遇到问题后只能靠瞎猜，东碰西撞，最后瞎猫碰到死耗子。&lt;/p&gt;

&lt;p&gt;GitHub 的&lt;code&gt;HTTPS&lt;/code&gt;协议和&lt;code&gt;SSH&lt;/code&gt;协议，这本来就对应着两套完全独立的权限校验方式，而我在&lt;code&gt;HTTPS&lt;/code&gt;协议不正常的情况下还去查看&lt;code&gt;SSH&lt;/code&gt;协议，这本来就实属多余。&lt;/p&gt;

&lt;p&gt;借助这次“掉坑”的经历，我对&lt;code&gt;Git&lt;/code&gt;权限校验的两种方式重头进行了梳理，并单独写了一篇博客，《深入浅出 Git 权限校验》，虽然花了些时间，但总算是扫清了萦绕多年的迷雾，感觉倍儿爽！&lt;/p&gt;

&lt;p&gt;如果你也对&lt;code&gt;Git的权限校验&lt;/code&gt;没有清晰的了解，遇到权限校验出错时只能“换一种方法试试”，也不知道怎么让一台计算机同时支持多个 GitHub 账号，那么也推荐看下那篇博客。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="Read More ..."&gt;Read More ...&lt;/h2&gt;
&lt;p&gt;公众号：&lt;a href="http://debugtalk.com/assets/images/wechat_qrcode.png" rel="nofollow" target="_blank" title=""&gt;DebugTalk&lt;/a&gt;
原文链接：&lt;a href="http://debugtalk.com/post/trap-in-GitHub-authority-verification" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/trap-in-GitHub-authority-verification&lt;/a&gt;
在微信公众号&lt;code&gt;debugtalk&lt;/code&gt;中输入&lt;code&gt;Git权限校验&lt;/code&gt;，获取《深入浅出 Git 权限校验》。&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Wed, 15 Jun 2016 17:57:34 +0800</pubDate>
      <link>https://ruby-china.org/topics/30286</link>
      <guid>https://ruby-china.org/topics/30286</guid>
    </item>
    <item>
      <title>关于促销活动页面测试的那些事儿</title>
      <description>&lt;p&gt;这篇文章来说说促销活动页面测试的那些事儿。&lt;/p&gt;
&lt;h2 id="什么是促销活动页面？"&gt;什么是促销活动页面？&lt;/h2&gt;
&lt;p&gt;通常电商平台在节假日会做一些促销活动，而活动的宣传方式，主要会采用 H5 静态页面的形式，也就是本文中要讲的促销活动页面。&lt;/p&gt;

&lt;p&gt;这些活动页面的特点是元素构成很简单，在页面中只包含一些促销商品的图片及其价格，而且价格往往都是写死在页面中，不会涉及到从数据库中读取，完完全全的静态页面；当然，促销活动页面的目的是将用户流量导向电商平台，因此在页面的图片或购买按钮背后会配上 URL 链接，用户点击链接后会跳转至电商平台对应的购买页面，活动页面也就完成了使命，这往往就是活动页面的全部内容。&lt;/p&gt;

&lt;p&gt;这么看来，促销活动页面跟街上发的传单非常相似，只是传单是纸质的，而活动页面是网页的罢了。&lt;/p&gt;
&lt;h2 id="促销活动页面为什么要这么做呢？"&gt;促销活动页面为什么要这么做呢？&lt;/h2&gt;
&lt;p&gt;四方面原因。&lt;/p&gt;

&lt;p&gt;第一，促销活动页面的目的性很强，就是为了主推几款特价商品，因此，商品和价格完全写死也不会有任何问题。&lt;/p&gt;

&lt;p&gt;第二，促销活动页面的时效性很强，为促销活动而生，生命周期就那么几天，促销活动结束后这个活动页面也就作废了，因此也不用考虑复用的问题。&lt;/p&gt;

&lt;p&gt;第三，从投入的人力成本和工作量考虑，促销活动页面的制作只需要设计师和前端工程师就能完成，无需后台开发人员进行配合，也无需对当前的电商平台进行任何功能调整。&lt;/p&gt;

&lt;p&gt;第四，电商平台在做促销活动时，往往会通过各种渠道进行推广，因此活动页面的访问流量是非常巨大的；而采用静态页面的形式，不仅可以极大地提高页面加载速度（图片等静态资源可以通过 CDN 存储），而且可以极大地减轻流量对电商平台服务器端的压力（用户在浏览活动页面的时候并不会与电商平台进行任何交互）。至于点击链接进入电商平台的流量嘛，毕竟转换率总是存在的，转换以后的流量会小很多，而这部分流量才是真正有效的。&lt;/p&gt;
&lt;h2 id="如何对活动页面进行测试"&gt;如何对活动页面进行测试&lt;/h2&gt;
&lt;p&gt;那么，就这么简单的一个静态页面，还需要对它进行测试么？&lt;/p&gt;

&lt;p&gt;咋一看，貌似还真没有可测试的内容，因为页面中就找不到一个真正意义上的功能点。&lt;/p&gt;

&lt;p&gt;然而，历史经验表明，在软件工程中无论多简单的功能，都是有可能出现 bug 的。针对活动页面的形式，我们需要重点关注如下几点。&lt;/p&gt;
&lt;h3 id="1、商品信息一致性"&gt;1、商品信息一致性&lt;/h3&gt;
&lt;p&gt;促销活动页面中的商品信息都是由前端工程师写死的，而非从电商平台的数据库中读取后进行展示。因此，在实际操作中，活动页面上的信息，特别是价格数据，有可能和电商平台中的商品不一致，这个是我们在测试的时候需要重点关注的。&lt;/p&gt;

&lt;p&gt;测试方式很简单，依次点击各个商品的链接，验证跳转的商品页面是否与促销活动页面中的商品信息一致即可。&lt;/p&gt;
&lt;h3 id="2、页面跳转行为一致性"&gt;2、页面跳转行为一致性&lt;/h3&gt;
&lt;p&gt;点击链接跳转页面，应该算是活动页面中唯一具有动作行为的功能了。而对于链接跳转而言，会存在两种形式，一种是在当前页面中加载商品页面，另一种是在新窗口中加载商品页面。&lt;/p&gt;

&lt;p&gt;本来两种形式区别并不大，采用哪种形式都可以，但是从追求完美的角度出发，我们还是需要保证活动页面中的所有链接的跳转行为都是相同的。&lt;/p&gt;

&lt;p&gt;因此，在测试时，逐一点击所有的链接，验证所有链接跳转行为是否一致即可。&lt;/p&gt;
&lt;h3 id="3、页面兼容性"&gt;3、页面兼容性&lt;/h3&gt;
&lt;p&gt;作为促销活动页面，虽然商品信息是最核心的内容，但是为了能吸引尽量多的用户，页面的设计往往花了很多功夫，力求精美。然而，用户访问活动页面的设备和浏览器五花八门，有可能是采用 PC 浏览器，有可能是采用 iPhone 设备，也可能是采用各种品牌和型号的 Android 设备，精心设计的活动页面在某些设备或浏览器上很有可能就出现样式混乱的情况。因此，促销活动页面的浏览器兼容性也是 z 在测试时需要重点关注的。&lt;/p&gt;

&lt;p&gt;差异在哪儿呢？差异就是不同的浏览器内核，不同的设备操作系统，不同的屏幕分辨率。&lt;/p&gt;

&lt;p&gt;当然，我们也不可能在所有类型的设备和浏览器上都测一遍，但是主流的浏览器内核和移动设备还是要尽量覆盖的。&lt;/p&gt;

&lt;p&gt;推荐的测试方式如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;针对不同浏览器内核的测试，在电脑上采用 Chrome、IE、Firefox、Safari 浏览器分别加载活动页面；&lt;/li&gt;
&lt;li&gt;针对不同移动设备类型的测试，在 iPhone 和 Android 设备上加载活动页面，iPhone 和 Android 设备均只选一款即可；&lt;/li&gt;
&lt;li&gt;针对不同屏幕分辨率的测试，可以在 PC 浏览器中打开开发者工具，里面可以模拟不同分辨率的设备加载页面，前面提到的浏览器基本都支持这个功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4、文案准确性"&gt;4、文案准确性&lt;/h3&gt;
&lt;p&gt;对于促销活动而言，吸引眼球的文案肯定是必不可少的，这也是在测试时需要重点关注的。&lt;/p&gt;

&lt;p&gt;对于文案方面的测试，可以重点从以下几个方面进行考核。&lt;/p&gt;

&lt;p&gt;首先，由于文案通常是由需求方提供，而活动页面是由设计师或前端工程师制作，因此有可能在制作页面的过程中出现了偏差，这个需要测试时仔细核对。&lt;/p&gt;

&lt;p&gt;另外，文案中出现错别字的情况也比较多，这个也需要格外注意，尽量杜绝这样的低级错误。&lt;/p&gt;

&lt;p&gt;还有一种情况，活动促销页面是面向某个国家的用户，语言可能是非汉语也非英语，这个时候周围谁也看不懂文案里面到底写的是啥。这个时候，只能请需求方再三进行确认，文案描述正确是一方面，另一方面就是需要考虑到目标国家的地域文化，不要出现产生歧义和误解的情况。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;以上内容便是从促销活动页面进行展开，联想到的关于测试的一些内容。&lt;/p&gt;

&lt;p&gt;可以看出，即使是再简单的东西，也是需要进行测试的，而且测试需要考虑的因素也非常多。另一方面，这也说明测试并不应该仅仅局限于技术层面，只有当我们站在业务和质量保障的角度，才会有更开阔的视野。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="Read More ..."&gt;Read More ...&lt;/h2&gt;
&lt;p&gt;公众号：&lt;a href="http://debugtalk.com/assets/images/wechat_qrcode.png" rel="nofollow" target="_blank" title=""&gt;DebugTalk&lt;/a&gt;
原文链接：&lt;a href="http://debugtalk.com/post/promotion-page-test" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/promotion-page-test&lt;/a&gt;&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Fri, 10 Jun 2016 14:43:04 +0800</pubDate>
      <link>https://ruby-china.org/topics/30242</link>
      <guid>https://ruby-china.org/topics/30242</guid>
    </item>
    <item>
      <title>从 0 到 1 搭建移动 App 功能自动化测试平台 (3)：编写 iOS 自动化测试脚本</title>
      <description>&lt;p&gt;通过前面三篇文章，我们已经将 iOS 自动化功能测试的开发环境全部准备就绪，也学习了 iOS UI 控件交互操作的一般性方法，接下来，就可以开始编写自动化测试脚本了。&lt;/p&gt;

&lt;p&gt;在本文中，我将在 M 项目中挑选一个功能点，对其编写自动化测试脚本，演示编写自动化测试用例的整个流程。&lt;/p&gt;
&lt;h2 id="语言的选择：Python or Ruby？"&gt;语言的选择：Python or Ruby？&lt;/h2&gt;
&lt;p&gt;之前介绍 Appium 的时候也提到，Appium 采用 Client-Server 的架构设计，并采用标准的 HTTP 通信协议；Client 端基本上可以采用任意主流编程语言编写测试用例，包括但不限于 C#、Ruby、Objective-C、Java、node.js、Python、PHP。&lt;/p&gt;

&lt;p&gt;因此，在开始编写自动化测试脚本之前，首先需要选定一门编程语言。&lt;/p&gt;

&lt;p&gt;这个选择因人而异，并不涉及到太大的优劣之分，基本上在上述几门语言中选择自己最熟悉的就好。&lt;/p&gt;

&lt;p&gt;但对我而言，选择却没有那么干脆，前段时间在 Python 和 Ruby 之间犹豫了很久，经过艰难的决定，最终选择了 Ruby。为什么不考虑 Java？不熟是一方面，另一方面是觉得采用编译型语言写测试用例总感觉太重，这活儿还是解释型语言来做更合适些。&lt;/p&gt;

&lt;p&gt;其实，最开始本来是想选择 Python 的，因为 Python 在软件测试领域比 Ruby 应用得更广，至少在国内，不管是公司团队，还是测试人员群体，使用 Python 的会比使用 Ruby 的多很多。&lt;/p&gt;

&lt;p&gt;那为什么还是选择了 Ruby 呢？&lt;/p&gt;

&lt;p&gt;我主要是基于如下几点考虑的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;从 Appium 的官方文档来看，Appium 对 Ruby 的支持力度，或者说是偏爱程度，貌似会更大些；在&lt;a href="http://appium.io/downloads.html" rel="nofollow" target="_blank" title=""&gt;Appium Client Libraries&lt;/a&gt;列表中将 Ruby 排在第一位就不说了，在&lt;a href="http://appium.io/tutorial.html?lang=en" rel="nofollow" target="_blank" title=""&gt;Appium Tutorials&lt;/a&gt;中示例语言就只采用了 Ruby 和 Java 进行描述。&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/appium/ruby_lib" rel="nofollow" target="_blank" title=""&gt;Appium_Console&lt;/a&gt;是采用 Ruby 编写的，在 Console 中执行的命令基本上可直接用在 Ruby 脚本中。&lt;/li&gt;
&lt;li&gt;后续打算引入 BDD（行为驱动开发）的测试模式，而不管是 cucumber 还是 RSpec，都是采用 Ruby 开发的。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;当然，还有最最重要的一点，身处于珠江三角洲最大的 Ruby 阵营，周围 Ruby 大牛云集，公司的好多业务系统也都是采用 Rails 作为后台语言，完全没理由不选择 Ruby 啊（测试用例还可以拉人一起写啊^_^）。&lt;/p&gt;
&lt;h2 id="第一个测试用例：系统登录"&gt;第一个测试用例：系统登录&lt;/h2&gt;
&lt;p&gt;在测试领域中，系统登录这个功能点的地位，堪比软件开发中的&lt;code&gt;Hello World&lt;/code&gt;，因此第一个测试用例就毫无悬念地选择系统登录了。&lt;/p&gt;

&lt;p&gt;在编写自动化测试脚本之前，我们首先需要清楚用例执行的路径，路径中操作涉及到的控件，以及被操作控件的属性信息。&lt;/p&gt;

&lt;p&gt;对于本次演示的 APP 来说，登录时需要先进入【My Account】页面，然后点击【Login】进入登录页面，接着在登录页面中输入账号密码后再点击【Login】按钮，完成登录操作。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/DJI_Plus_Login.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;确定了操作路径以后，就可以在&lt;code&gt;Appium Ruby Console&lt;/code&gt;中依次操作一遍，目的是确保代码能正确地对控件进行操作。&lt;/p&gt;

&lt;p&gt;第一步要点击【My Account】按钮，因此先查看下 Button 控件属性。要是不确定目标控件的类型，可以直接执行&lt;code&gt;page&lt;/code&gt;命令，然后在返回结果中根据控件名称进行查找。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;1] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; page :button
...（略）
UIAButton
   name, label: My Account
   &lt;span class="nb"&gt;id&lt;/span&gt;: My Account &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; My Account
nil
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过返回结果，可以看到【My Account】按钮的 name、label 属性就是“My Account”，因此可以通过&lt;code&gt;button_exact('My Account')&lt;/code&gt;方式来定位控件，并进行点击操作。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;2] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; button_exact&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'My Account'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.click
nil
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行命令后，观察 iOS 模拟器中 APP 的响应情况，看是否成功进入“My Account”页面。&lt;/p&gt;

&lt;p&gt;第二步也是类似的，操作代码如下：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;3] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; button_exact&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.click
nil
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入到登录页面后，再次查看页面中的控件信息：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;4] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; page
...（略）
UIATextField
   value: Email Address
   &lt;span class="nb"&gt;id&lt;/span&gt;: Email Address &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; Email Address
UIASecureTextField
   value: Password &lt;span class="o"&gt;(&lt;/span&gt;6-16 characters&lt;span class="o"&gt;)&lt;/span&gt;
   &lt;span class="nb"&gt;id&lt;/span&gt;: Password &lt;span class="o"&gt;(&lt;/span&gt;6-16 characters&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; Password &lt;span class="o"&gt;(&lt;/span&gt;6-16 characters&lt;span class="o"&gt;)&lt;/span&gt;
UIAButton
   name, label: Login
   &lt;span class="nb"&gt;id&lt;/span&gt;: Log In &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; Login
       登录     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; Login
...（略）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三步需要填写账号密码，账号密码的控件属性分别是&lt;code&gt;UIATextField&lt;/code&gt;和&lt;code&gt;UIASecureTextField&lt;/code&gt;。由于这两个控件的类型在登录页面都是唯一的，因此可以采用控件的类型来进行定位，然后进行输入操作，代码如下：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;5] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; tag&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIATextField'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.type &lt;span class="s1"&gt;'leo.lee@dji.com'&lt;/span&gt;
&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;6] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; tag&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIASecureTextField'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.type &lt;span class="s1"&gt;'123456'&lt;/span&gt;
&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完输入命令后，在 iOS 模拟器中可以看到账号密码输入框都成功输入了内容。&lt;/p&gt;

&lt;p&gt;最后第四步点击【Login】按钮，操作上和第二步完全一致。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;7] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; button_exact&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.click
nil
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完以上四个步骤后，在 iOS 模拟器中看到成功完成账号登录操作，这说明我们的执行命令没有问题，可以用于编写自动化测试代码。整合起来，测试脚本就是下面这样。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;button_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'My Account'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="n"&gt;button_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIATextField'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'leo.lee@dji.com'&lt;/span&gt;
&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIASecureTextField'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'12345678'&lt;/span&gt;
&lt;span class="n"&gt;button_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将以上脚本保存为&lt;code&gt;login.rb&lt;/code&gt;文件。&lt;/p&gt;

&lt;p&gt;但当我们直接运行&lt;code&gt;login.rb&lt;/code&gt;文件时，并不能运行成功。原因很简单，脚本中的&lt;code&gt;button_exact&lt;/code&gt;、&lt;code&gt;tag&lt;/code&gt;这些方法并没有定义，我们在文件中也没有引入相关库文件。&lt;/p&gt;

&lt;p&gt;在上一篇文章中有介绍过，通过&lt;code&gt;arc&lt;/code&gt;启动虚拟机时，会从&lt;code&gt;appium.txt&lt;/code&gt;中读取虚拟机的配置信息。类似的，我们在脚本中执行自动化测试时，也会加载虚拟机，因此同样需要在脚本中指定虚拟机的配置信息，并初始化&lt;code&gt;Appium Driver&lt;/code&gt;的实例。&lt;/p&gt;

&lt;p&gt;初始化代码可以通过&lt;code&gt;Appium Inspector&lt;/code&gt;生成，基本上为固定模式，我们暂时不用深究。&lt;/p&gt;

&lt;p&gt;添加初始化部分的代码后，测试脚本如下所示。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'rubygems'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'appium_lib'&lt;/span&gt;

&lt;span class="n"&gt;capabilities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;'appium-version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1.0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'platformName'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'iOS'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'platformVersion'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'9.3'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;server_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://0.0.0.0:4723/wd/hub"&lt;/span&gt;
&lt;span class="no"&gt;Appium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;caps: &lt;/span&gt;&lt;span class="n"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;start_driver&lt;/span&gt;
&lt;span class="no"&gt;Appium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;promote_appium_methods&lt;/span&gt; &lt;span class="no"&gt;Object&lt;/span&gt;

&lt;span class="c1"&gt;# testcase: login&lt;/span&gt;
&lt;span class="n"&gt;button_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'My Account'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="n"&gt;button_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIATextField'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'leo.lee@dji.com'&lt;/span&gt;
&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIASecureTextField'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'123456'&lt;/span&gt;
&lt;span class="n"&gt;button_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;

&lt;span class="n"&gt;driver_quit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="优化测试脚本：加入等待机制"&gt;优化测试脚本：加入等待机制&lt;/h2&gt;
&lt;p&gt;如上测试脚本编写好后，在 Terminal 中运行&lt;code&gt;ruby login.rb&lt;/code&gt;，就可以执行脚本了。&lt;/p&gt;

&lt;p&gt;运行命令后，会看到 iOS 虚拟机成功启动，接着 App 成功进行加载，然后自动按照前面设计的路径，执行系统登录流程。&lt;/p&gt;

&lt;p&gt;但是，在实际操作过程中，发现有时候运行脚本时会出现找不到控件的异常，异常信息如下所示：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ ruby login.rb
/Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/common/helper.rb:218:in &lt;span class="sb"&gt;`&lt;/span&gt;_no_such_element&lt;span class="s1"&gt;': An element could not be located on the page using the given search parameters. (Selenium::WebDriver::Error::NoSuchElementError)
    from /Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/ios/helper.rb:578:in `ele_by_json'&lt;/span&gt;
    from /Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/ios/helper.rb:367:in &lt;span class="sb"&gt;`&lt;/span&gt;ele_by_json_visible_exact&lt;span class="s1"&gt;'
    from /Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/ios/element/button.rb:41:in `button_exact'&lt;/span&gt;
    from /Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/driver.rb:226:in &lt;span class="sb"&gt;`&lt;/span&gt;rescue &lt;span class="k"&gt;in &lt;/span&gt;block &lt;span class="o"&gt;(&lt;/span&gt;4 levels&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;promote_appium_methods&lt;span class="s1"&gt;'
    from /Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/driver.rb:217:in `block (4 levels) in promote_appium_methods'&lt;/span&gt;
    from login.rb:28:in &lt;span class="sb"&gt;`&lt;/span&gt;&amp;lt;main&amp;gt;&lt;span class="s1"&gt;'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更奇怪的是，这个异常并不是稳定出现的，有时候能正常运行整个用例，但有时在某个步骤就会抛出找不到控件的异常。这是什么原因呢？为什么在&lt;code&gt;Appium Ruby Console&lt;/code&gt;中单步操作时就不会出现这个问题，但是在执行脚本的时候就会偶尔出现异常呢？&lt;/p&gt;

&lt;p&gt;原来，在我们之前的脚本中，两条命令之间并没有间隔时间，有可能前一条命令执行完后，模拟器中的应用还没有完成下一个页面的加载，下一条命令就又开始查找控件，然后由于找不到控件就抛出异常了。&lt;/p&gt;

&lt;p&gt;这也是为什么我们在&lt;code&gt;Appium Ruby Console&lt;/code&gt;中没有出现这样的问题。因为手工输入命令多少会有一些耗时，输入两条命令的间隔时间足够虚拟机中的 APP 完成下一页面的加载了。&lt;/p&gt;

&lt;p&gt;那针对这种情况，我们要怎么修改测试脚本呢？难道要在每一行代码之间都添加休眠（sleep）函数么？&lt;/p&gt;

&lt;p&gt;也不用这么麻烦，针对这类情况，&lt;code&gt;ruby_lib&lt;/code&gt;实现了&lt;code&gt;wait&lt;/code&gt;机制。将执行命令放入到&lt;code&gt;wait{}&lt;/code&gt;中后，执行脚本时就会等待该命令执行完成后再去执行下一条命令。当然，等待也不是无休止的，如果等待 30 秒后还是没有执行完，仍然会抛出异常。&lt;/p&gt;

&lt;p&gt;登录流程的测试脚本修改后如下所示（已省略初始化部分的代码）：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;button_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'My Account'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;button_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIATextField'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'leo.lee@dji.com'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIASecureTextField'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'123456'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;button_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对脚本添加&lt;code&gt;wait&lt;/code&gt;机制后，之前出现的找不到控件的异常就不再出现了。&lt;/p&gt;
&lt;h2 id="优化测试脚本：加入结果检测机制"&gt;优化测试脚本：加入结果检测机制&lt;/h2&gt;
&lt;p&gt;然而，现在脚本仍然不够完善。&lt;/p&gt;

&lt;p&gt;我们在&lt;code&gt;Appium Ruby Console&lt;/code&gt;中手工执行命令后，都是由人工肉眼确认虚拟机中 APP 是否成功进入下一个页面，或者返回结果是否正确。&lt;/p&gt;

&lt;p&gt;但是在执行自动化测试脚本时，我们不可能一直去盯着模拟器。因此，我们还需要在脚本中加入结果检测机制，通过脚本实现结果正确性的检测。&lt;/p&gt;

&lt;p&gt;具体怎么做呢？&lt;/p&gt;

&lt;p&gt;原理也很简单，只需要在下一个页面中，寻找一个在前一个页面中没有的控件。&lt;/p&gt;

&lt;p&gt;例如，由 A 页面跳转至 B 页面，在 B 页面中会存在“Welcome”的文本控件，但是在 A 页面中是没有这个“Welcome”文本控件的；那么，我们就可以在脚本中的跳转页面语句之后，加入一条检测“Welcome”文本控件的语句；后续在执行测试脚本的时候，如果页面跳转失败，就会因为找不到控件而抛出异常，我们也能通过这个异常知道测试执行失败了。&lt;/p&gt;

&lt;p&gt;当然，对下一页面中的控件进行检测时同样需要加入等待机制的。&lt;/p&gt;

&lt;p&gt;登录流程的测试脚本修改后如下所示（已省略初始化部分的代码）：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;button_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'My Account'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;text_exact&lt;/span&gt; &lt;span class="s1"&gt;'System Settings'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;button_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;button_exact&lt;/span&gt; &lt;span class="s1"&gt;'Forget password?'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIATextField'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'leo.lee@dji.com'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIASecureTextField'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'12345678'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;button_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;text_exact&lt;/span&gt; &lt;span class="s1"&gt;'My Message'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，系统登录流程的自动化测试脚本我们就编写完成了。&lt;/p&gt;
&lt;h2 id="To be continued ..."&gt;To be continued ...&lt;/h2&gt;
&lt;p&gt;在本文中，我们通过系统登录这一典型功能点，演示了编写自动化测试用例的整个流程。&lt;/p&gt;

&lt;p&gt;在下一篇文章中，我们还会对自动化测试脚本的结构进行进一步优化，并实现测试代码工程化。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="Read More ..."&gt;Read More ...&lt;/h2&gt;
&lt;p&gt;公众号：&lt;a href="http://debugtalk.com/assets/images/wechat_qrcode.png" rel="nofollow" target="_blank" title=""&gt;DebugTalk&lt;/a&gt;
原文链接：&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-write-iOS-testcase-scripts" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-write-iOS-testcase-scripts&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="相关文章"&gt;相关文章&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-backgroud-introduction" rel="nofollow" target="_blank" title=""&gt;《从 0 到 1 搭建移动 App 功能自动化测试平台（0）背景介绍和平台规划》&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-Appium-inspector-iOS-simulator" rel="nofollow" target="_blank" title=""&gt;《从 0 到 1 搭建移动 App 功能自动化测试平台（1）模拟器中运行 iOS 应用》&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-Appium-interrogate-iOS-UI" rel="nofollow" target="_blank" title=""&gt;《从 0 到 1 搭建移动 App 功能自动化测试平台（2）操作 iOS 应用的控件》&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>debugtalk</author>
      <pubDate>Mon, 30 May 2016 19:20:23 +0800</pubDate>
      <link>https://ruby-china.org/topics/30163</link>
      <guid>https://ruby-china.org/topics/30163</guid>
    </item>
    <item>
      <title>从 0 到 1 搭建移动 App 功能自动化测试平台 (2)：操作 iOS 应用的控件</title>
      <description>&lt;h2 id="写在前面"&gt;写在前面&lt;/h2&gt;
&lt;p&gt;前两天微信突然发来一条系统消息，提示&lt;code&gt;DebugTalk&lt;/code&gt;可以开通原创标识了（同时也有了评论功能），虽然一直在期待，但没想到来得这么快，着实是个不小的惊喜。&lt;/p&gt;

&lt;p&gt;另外，最近在公众号后台也收到好几个朋友的信息，有的是询问某某部分什么时候能发布，有的是希望能加快更新速度。说实话，收到这样的信息虽然会有压力，但真的挺开心的，因为这说明&lt;code&gt;DebugTalk&lt;/code&gt;至少能给一部分人带去价值，这说明这件事本身还是值得坚持去做的。&lt;/p&gt;

&lt;p&gt;不过，在更新频率这件事儿上，的确是要跟大家说抱歉了。因为&lt;code&gt;DebugTalk&lt;/code&gt;发布的内容全都是原创，主题基本上都是来源于我日常测试工作的经验积累，或者我近期学习一些测试技术的收获总结，这也意味着，我写的东西很多时候并不是自己完全熟悉的（完全掌握的东西也没有足够的动力专门花时间去写）。&lt;/p&gt;

&lt;p&gt;就拿最近连载的《从 0 到 1 搭建移动 App 功能自动化测试平台》系列来说，由于我也是边探索边总结，因此中途难免会遇到一些意想不到的坑，造成额外的耗时，而且为了保证文章能尽量通俗易通，我也需要对涉及到的内容充分进行理解，并且经过大量实践进行验证，然后才能站在半个初学者、半个过来人的角度，重新整理思路，最后以尽可能流畅的思路将主题内容讲解清楚。&lt;/p&gt;

&lt;p&gt;基于这些原因，&lt;code&gt;DebugTalk&lt;/code&gt;要做到每日更新是很难了，但是保证每周发布 1~2 篇还是可以的，希望大家能理解。&lt;/p&gt;
&lt;h2 id="关于UI控件"&gt;关于 UI 控件&lt;/h2&gt;
&lt;p&gt;在&lt;a href="http://mp.weixin.qq.com/s?__biz=MzAxMTczMjgzMQ==&amp;amp;mid=2650587833&amp;amp;idx=1&amp;amp;sn=b4cb2a232418ca6c297a99b7628d64bb#rd" rel="nofollow" target="_blank" title=""&gt;上一篇文章&lt;/a&gt;中，我们成功地通过 Appium Inspector 调用模拟器并运行 iOS 应用，iOS 的自动化测试环境也已全部准备就绪了。&lt;/p&gt;

&lt;p&gt;那么接下来，我们就可以开始实现自动化测试了么？&lt;/p&gt;

&lt;p&gt;貌似还不行。在开始之前，我们先想下什么是 APP 功能自动化测试。&lt;/p&gt;

&lt;p&gt;APP 的功能自动化测试，简单地来说，就是让功能测试用例自动地在 APP 上执行。具体到每一个测试用例，就是能模拟用户行为对 UI 控件进行操作，自动化地实现一个功能点或者一个流程的操作。再细分到每一步，就是对 UI 控件进行操作。&lt;/p&gt;

&lt;p&gt;因此，在正式开始编写自动化测试用例之前，我们还需要熟悉如何与 APP 的 UI 控件进行交互操作。&lt;/p&gt;

&lt;p&gt;在 iOS 系统中，UI 控件有多种类型，常见的有按钮（UIAButton）、文本（UIAStaticText）、输入框（UIATextField）等等。但不管是对什么类型的 UI 控件进行操作，基本都可以分解为三步，首先是获取目标控件的属性信息，然后是对目标控件进行定位，最后是对定位到的控件执行动作。&lt;/p&gt;
&lt;h2 id="获取UI控件信息"&gt;获取 UI 控件信息&lt;/h2&gt;
&lt;p&gt;在 Appium 中，要获取 iOS 的 UI 控件元素信息，可以采用两种方式：一种是在前一篇文章中提到的 Appium Inspector，另一种是借助 Ruby 实现的&lt;code&gt;appium_console&lt;/code&gt;，在 Terminal 中通过命令进行查询。&lt;/p&gt;
&lt;h3 id="Appium Inspector"&gt;Appium Inspector&lt;/h3&gt;
&lt;p&gt;运行 Appium Server，并启动【Inspector】后，整体界面如下图所示。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/Appium_inspector_introduction.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;现对照着这张图对 Appium Inspector 进行介绍。&lt;/p&gt;

&lt;p&gt;在右边部分，是启动的模拟器，里面运行着我们的待测 APP。我们可以像在真机中一样，在模拟器中执行任意功能的操作，当然，模拟器跟真机毕竟还是有区别的，跟传感器相关的功能，例如摄像头、重力感应等，是没法实现的。&lt;/p&gt;

&lt;p&gt;在左边部分，就是&lt;code&gt;Appium Inspector&lt;/code&gt;。Inspector 主要由如下四个部分组成：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;预览界面区：显示画面与模拟器界面一致；不过，当我们在模拟器中切换界面后，Inspector 的预览区中显示图像并不会自动同步，若要同步，需要点击【Refresh】按钮，然后 Inspector 会将模拟器当前 UI 信息 dump 后显示到预览区；在预览区中，可以点击选择任意 UI 控件。&lt;/li&gt;
&lt;li&gt;UI 信息展示区：展示当前界面预览区中所有 UI 元素的层级关系和 UI 元素的详细信息；在预览区中点击选择任意 UI 控件后，在“Details”信息框中展示选中控件的详细信息，包括 name、label、value、xpath 等属性值；通过层级关系，我们也能了解选中控件在当前界面树状结构中所处的具体位置。&lt;/li&gt;
&lt;li&gt;交互操作区：模拟用户在设备上的操作，例如单击（tap）、滑动（swipe）、晃动（shake）、输入（input）等；操作动作是针对预览界面区选中的控件，因此在操作之前，务必需要先在预览区点击选择 UI 元素。&lt;/li&gt;
&lt;li&gt;脚本生成区：将用户行为转换为脚本代码；点击【Record】按钮后，会弹出代码区域；在交互操作区进行操作后，就会实时生成对应的脚本代码；代码语言可通过下拉框进行选择，当前支持的语言类型有：C#、Ruby、Objective-C、Java、node.js、Python。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在实践操作中，Inspector 最大的用途就是在可以可视化地查看 UI 元素信息，并且可以将操作转换为脚本代码，这对初学者尤为有用。&lt;/p&gt;

&lt;p&gt;例如，在预览区点击选中按钮“BUY NOW”，然后在 UI 信息展示区的 Details 窗口就可以看到该按钮的所有属性信息。在交互操作区点击【Tap】按钮后，就会模拟用户点击“BUY NOW”按钮，并且在脚本区域生成当次按钮点击的脚本（选择 Ruby 语言）：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"BUY NOW &amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上就是使用&lt;code&gt;Appium Inspector&lt;/code&gt;的一般性流程。&lt;/p&gt;
&lt;h3 id="Appium Ruby Console"&gt;Appium Ruby Console&lt;/h3&gt;
&lt;p&gt;有了&lt;code&gt;Appium Inspector&lt;/code&gt;，为什么还需要&lt;code&gt;Appium Ruby Console&lt;/code&gt;呢？&lt;/p&gt;

&lt;p&gt;其实，&lt;code&gt;Appium Ruby Console&lt;/code&gt;也并不是必须的。经过与多个熟悉&lt;code&gt;Appium&lt;/code&gt;的前辈交流，他们也从未用过&lt;code&gt;Appium Ruby Console&lt;/code&gt;，这说明&lt;code&gt;Appium Ruby Console&lt;/code&gt;并不是必须的，没有它也不会影响我们对&lt;code&gt;Appium&lt;/code&gt;的使用。&lt;/p&gt;

&lt;p&gt;但是，这并不意味着&lt;code&gt;Appium Ruby Console&lt;/code&gt;是多余的。经过这些天对&lt;code&gt;Appium&lt;/code&gt;的摸索，我越发地喜欢上&lt;code&gt;Appium Ruby Console&lt;/code&gt;，并且使用的频率越来越高，现在已基本上很少使用&lt;code&gt;Appium Inspector&lt;/code&gt;了。这种感觉怎么说呢？&lt;code&gt;Inspector&lt;/code&gt;相比于&lt;code&gt;Ruby Conosle&lt;/code&gt;，就像是&lt;code&gt;GUI&lt;/code&gt;相比于&lt;code&gt;Linux Terminal&lt;/code&gt;，大家应该能体会了吧。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Appium Inspector&lt;/code&gt;的功能是很齐全，GUI 操作也很方便，但是，最大的问题就是使用的时候非常慢，在预览界面区切换一个页面常常需要好几秒，甚至数十秒，这是很难让人接受的。&lt;/p&gt;

&lt;p&gt;在上一节中也说到了，Inspector 最大的用途就是在可以可视化地查看 UI 元素信息，并且可以将操作转换为脚本代码。但是当我们对&lt;code&gt;Appium&lt;/code&gt;的常用 API 熟悉以后，我们就不再需要由工具来生成脚本，因为自己直接写会更快，前提是我们能知道目标控件的属性信息（type、name、label、value）。&lt;/p&gt;

&lt;p&gt;在这种情况下，如果能有一种方式可以供我们快速查看当前屏幕的控件属性信息，那该有多好。&lt;/p&gt;

&lt;p&gt;庆幸的是，在阅读&lt;code&gt;Appium&lt;/code&gt;官方文档时，发现&lt;code&gt;Appium&lt;/code&gt;的确是支持命令行方式的，这就是&lt;code&gt;Appium Ruby Console&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Appium Ruby Console&lt;/code&gt;是采用 Ruby 语言开发的，在使用方式上面和 Ruby 的&lt;code&gt;irb&lt;/code&gt;很类似。&lt;/p&gt;

&lt;p&gt;在使用&lt;code&gt;Appium Ruby Console&lt;/code&gt;时，虚拟机的配置信息并不会从 GUI 中读取，而是要通过配置文件进行指定。&lt;/p&gt;

&lt;p&gt;配置文件的名称统一要求为&lt;code&gt;appium.txt&lt;/code&gt;，内容形式如下所示：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[caps]
platformName = "ios"
platformVersion = '9.3',
app = "/path/to/UICatalog.app.zip"
deviceName = "iPhone Simulator"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;platformName&lt;/code&gt;指定虚拟机操作系统类型，“ios”或者"android"；&lt;code&gt;platformVersion&lt;/code&gt;指定操作系统的版本，例如 iOS 的'9.3'，或者 Android 的'5.1'；&lt;code&gt;app&lt;/code&gt;指定被测应用安装包的路径。这三个参数是必须的，与 Inspector 中的配置也能对应上。&lt;/p&gt;

&lt;p&gt;在使用&lt;code&gt;Appium Ruby Console&lt;/code&gt;时，首先需要启动&lt;code&gt;Appium Server&lt;/code&gt;，通过&lt;code&gt;GUI&lt;/code&gt;或者&lt;code&gt;Terminal&lt;/code&gt;均可。&lt;/p&gt;

&lt;p&gt;然后，在 Terminal 中，进入到&lt;code&gt;appium.txt&lt;/code&gt;文件所在的目录，执行&lt;code&gt;arc&lt;/code&gt;命令即可启动&lt;code&gt;Appium Ruby Console&lt;/code&gt;。&lt;code&gt;arc&lt;/code&gt;，即是 appium ruby console 首字母的组合。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ &lt;span class="nb"&gt;ls
&lt;/span&gt;appium.txt
➜ arc
&lt;span class="o"&gt;[&lt;/span&gt;1] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，就可以通过执行命令查询当前设备屏幕中的控件信息。&lt;/p&gt;

&lt;p&gt;使用频率最高的一个命令是&lt;code&gt;page&lt;/code&gt;，通过这个命令可以查看到当前屏幕中所有控件的基本信息。&lt;/p&gt;

&lt;p&gt;例如，当屏幕停留在前面截图中的页面时，执行&lt;code&gt;page&lt;/code&gt;命令可以得到如下内容。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;1] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; page
UIANavigationBar
   name: HomeView
   &lt;span class="nb"&gt;id&lt;/span&gt;: Home &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; Home
       米    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; m
       去看看  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; View
UIAButton
   name, label: tabbar category gray
UIAImage
   name: dji_logo.png
UIAButton
   name, label: tabbar cart gray
UIATableView
   value: rows 1 to 4 of 15
UIAPageIndicator
   value: page 2 of 2
UIATableCell
   name: For the first &lt;span class="nb"&gt;time &lt;/span&gt;ever &lt;span class="k"&gt;in &lt;/span&gt;a hand held camera, the Osmo brings professional, realtime cinema-quality stabilization.
   &lt;span class="nb"&gt;id&lt;/span&gt;: 米 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; m
UIAStaticText
   name, label, value: For the first &lt;span class="nb"&gt;time &lt;/span&gt;ever &lt;span class="k"&gt;in &lt;/span&gt;a hand held camera, the Osmo brings professional, realtime cinema-quality stabilization.
   &lt;span class="nb"&gt;id&lt;/span&gt;: 米 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; m
UIAStaticText
   name, label, value: OSMO
UIAButton
   name, label: SHOP NOW &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
UIATableCell
   name: Ronin
UIAStaticText
   name, label, value: Ronin
UIAStaticText
   name, label, value: Phantom
   &lt;span class="nb"&gt;id&lt;/span&gt;: 米 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; m
... &lt;span class="o"&gt;(&lt;/span&gt;略&lt;span class="o"&gt;)&lt;/span&gt;
UIAButton
   name, label: Store
   value: 1
   &lt;span class="nb"&gt;id&lt;/span&gt;: 门店 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; Store
... &lt;span class="o"&gt;(&lt;/span&gt;略&lt;span class="o"&gt;)&lt;/span&gt;
UIAButton
   name, label: My Account
   &lt;span class="nb"&gt;id&lt;/span&gt;: My Account &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; My Account
nil
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过返回信息，我们就可以看到所有控件的 type、name、label、value 属性值。如果在某个控件下没有显示 label 或 value，这是因为这个值为空，我们可以不予理会。&lt;/p&gt;

&lt;p&gt;由于&lt;code&gt;page&lt;/code&gt;返回的信息太多，可能不便于查看，因此在使用&lt;code&gt;page&lt;/code&gt;命令时，也可以指定控件的类型，相当于对当前屏幕的控件进行筛选，只返回指定类型的控件信息。&lt;/p&gt;

&lt;p&gt;指定控件类型时，可以通过 string 类型进行指定（如 page "Image"），也可通过 symbol 类型进行指定（如 page :cell）。指定的类型可只填写部分内容，并且不分区大小写。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;2] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; page &lt;span class="s2"&gt;"Image"&lt;/span&gt;
UIAImage
   name: dji_logo.png
nil
&lt;span class="o"&gt;[&lt;/span&gt;3] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; page :cell
UIATableCell
   name: DJI’s smartest flying camera ever.
   &lt;span class="nb"&gt;id&lt;/span&gt;: 米 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; m
UIATableCell
   name: Ronin
UIATableCell
   name: Phantom
   &lt;span class="nb"&gt;id&lt;/span&gt;: 米 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; m
nil
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要查看当前屏幕的所有控件类型，可以执行&lt;code&gt;page_class&lt;/code&gt;命令进行查看。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;4] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; page_class
14x UIAButton
8x UIAStaticText
4x UIAElement
4x UIATableCell
2x UIAImage
2x UIAWindow
1x UIAPageIndicator
1x UIATableView
1x UIAStatusBar
1x UIANavigationBar
1x UIATabBar
1x UIAApplication
nil
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本上，&lt;code&gt;page&lt;/code&gt;返回的控件信息已经足够满足绝大多数场景需求，但有时候情况比较特殊，需要&lt;code&gt;enabled&lt;/code&gt;、&lt;code&gt;xpath&lt;/code&gt;、&lt;code&gt;visible&lt;/code&gt;、坐标等属性信息，这时就可以通过执行&lt;code&gt;source&lt;/code&gt;命令。执行&lt;code&gt;source&lt;/code&gt;命令后，就可以返回当前屏幕中所有控件的所有信息，以 xml 格式进行展现。&lt;/p&gt;
&lt;h2 id="定位UI控件"&gt;定位 UI 控件&lt;/h2&gt;
&lt;p&gt;获取到 UI 控件的属性信息后，就可以对控件进行定位了。&lt;/p&gt;

&lt;p&gt;首先介绍下最通用的定位方式，&lt;code&gt;find&lt;/code&gt;。通过&lt;code&gt;find&lt;/code&gt;命令，可以实现在控件的诸多属性值（&lt;code&gt;name&lt;/code&gt;、&lt;code&gt;label&lt;/code&gt;、&lt;code&gt;value&lt;/code&gt;、&lt;code&gt;hint&lt;/code&gt;）中查找目标值。查询时不区分大小写，如果匹配结果有多个，则只返回第一个结果。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;5] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; find&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'osmo'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;#&amp;lt;Selenium::WebDriver::Element:0x..febd52a30dcdfea32 id="2"&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;6] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; find&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'osmo'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.label
&lt;span class="s2"&gt;"Osmo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一个通用的定位方式是&lt;code&gt;find_element&lt;/code&gt;，它也可以实现对所有控件进行查找，但是相对于&lt;code&gt;find&lt;/code&gt;，可以对属性类型进行指定。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;7] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; find_element&lt;span class="o"&gt;(&lt;/span&gt;:class_name, &lt;span class="s1"&gt;'UIATextField'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;#&amp;lt;Selenium::WebDriver::Element:0x31d87e3848df8804 id="3"&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;8] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; find_element&lt;span class="o"&gt;(&lt;/span&gt;:class_name, &lt;span class="s1"&gt;'UIATextField'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.value
&lt;span class="s2"&gt;"Email Address"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过在实践中发现，采用&lt;code&gt;find&lt;/code&gt;、&lt;code&gt;find_element&lt;/code&gt;这类通用的定位方式并不好用，因为定位结果经常不是我们期望的。&lt;/p&gt;

&lt;p&gt;经过反复摸索，我推荐根据目标控件的类型，选择对应的定位方式。总结起来，主要有以下三种方式。&lt;/p&gt;

&lt;p&gt;针对 Button 类型的控件（UIAButton），采用&lt;code&gt;button_exact&lt;/code&gt;进行定位：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;9] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; button_exact&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;#&amp;lt;Selenium::WebDriver::Element:0x..feaebd8302b6d77cc id="4"&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;针对 Text 类型的控件（UIAStaticText），采用&lt;code&gt;text_exact&lt;/code&gt;进行定位：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;10] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; text_exact&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Phantom'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;#&amp;lt;Selenium::WebDriver::Element:0x1347e89100fdcee2 id="5"&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;针对控件类型进行定位时，采用&lt;code&gt;tag&lt;/code&gt;；如下方式等价于&lt;code&gt;find_element(:class_name, 'UIASecureTextField')&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;11] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; tag&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIASecureTextField'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;#&amp;lt;Selenium::WebDriver::Element:0x..fc6f5efd05a82cdca id="6"&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本上，这三种方式就已经足够应付绝大多数测试场景了。当然，这三种方式只是我个人经过实践后选择的定位方式，除了这三种，&lt;code&gt;Appium&lt;/code&gt;还支持很多种其它定位方式，大家可自行查看&lt;code&gt;Appium&lt;/code&gt;官方文档进行选择。&lt;/p&gt;

&lt;p&gt;另外，除了对控件进行定位，有时候我们还想判断当前屏幕中是否存在某个控件（通常用于结果检测判断），这要怎么做呢？&lt;/p&gt;

&lt;p&gt;一种方式是借助于&lt;code&gt;Appium&lt;/code&gt;的控件查找机制，即找不到控件时会抛出异常（&lt;code&gt;Selenium::WebDriver::Error::NoSuchElementError&lt;/code&gt;）；反过来，当查找某个控件抛出异常时，则说明当前屏幕中不存在该控件。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;12] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; button_exact&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login_invalid'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
Selenium::WebDriver::Error::NoSuchElementError: An element could not be located on the page using the given search parameters.
from /Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/common/helper.rb:218:in &lt;span class="sb"&gt;`&lt;/span&gt;_no_such_element&lt;span class="s1"&gt;'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该种方式可行，但比较暴力，基本上不会采用这种方式。&lt;/p&gt;

&lt;p&gt;另一种更好的方式是，查找当前屏幕中指定控件的个数，若个数不为零，则说明控件存在。具体操作上，将&lt;code&gt;button_exact&lt;/code&gt;替换为&lt;code&gt;buttons_exact&lt;/code&gt;，将&lt;code&gt;text_exact&lt;/code&gt;替换为&lt;code&gt;texts_exact&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;12] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; buttons_exact&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.count
1
&lt;span class="o"&gt;[&lt;/span&gt;13] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; buttons_exact&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login_invalid'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.count
0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除此之外，基于 Ruby 实现的&lt;code&gt;appium_lib&lt;/code&gt;还支持&lt;code&gt;exists&lt;/code&gt;方法，可直接返回 Boolean 值。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;14] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; exists &lt;span class="o"&gt;{&lt;/span&gt; button_exact&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;15] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; exists &lt;span class="o"&gt;{&lt;/span&gt; button_exact&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login_invalid'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="对控件执行操作"&gt;对控件执行操作&lt;/h2&gt;
&lt;p&gt;定位到具体的控件后，操作就比较容易了。&lt;/p&gt;

&lt;p&gt;操作类型不多，最常用就是点击（click）和输入（type），这两个操作能覆盖 80% 以上的场景。&lt;/p&gt;

&lt;p&gt;对于点击操作，才定位到的控件后面添加&lt;code&gt;.click&lt;/code&gt;方法；对于输入操作，在定位到的输入框控件后面添加&lt;code&gt;.type&lt;/code&gt;方法，并传入输入值。&lt;/p&gt;

&lt;p&gt;例如，账号登录操作就包含输入和点击两种操作类型。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;16] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; find_element&lt;span class="o"&gt;(&lt;/span&gt;:class_name, &lt;span class="s1"&gt;'UIATextField'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.type &lt;span class="s1"&gt;'leo.lee@dji.com'&lt;/span&gt;
&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;17] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; find_element&lt;span class="o"&gt;(&lt;/span&gt;:class_name, &lt;span class="s1"&gt;'UIASecureTextField'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.type &lt;span class="s1"&gt;'123456'&lt;/span&gt;
&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;18] pry&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; button_exact&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Login'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.click
nil
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="To be continued ..."&gt;To be continued ...&lt;/h2&gt;
&lt;p&gt;在本文中，我们学习了对 iOS UI 控件进行交互操作的一般性方法，为编写自动化测试脚本打好了基础。&lt;/p&gt;

&lt;p&gt;在下一篇文章中，我们就要正式开始针对 iOS 应用编写自动化测试脚本了。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="Read More ..."&gt;Read More ...&lt;/h2&gt;
&lt;p&gt;公众号：&lt;a href="http://debugtalk.com/assets/images/wechat_qrcode.png" rel="nofollow" target="_blank" title=""&gt;DebugTalk&lt;/a&gt;
原文链接：&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-Appium-interrogate-iOS-UI" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-Appium-interrogate-iOS-UI&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="相关文章"&gt;相关文章&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-backgroud-introduction" rel="nofollow" target="_blank" title=""&gt;《从 0 到 1 搭建移动 App 功能自动化测试平台（0）背景介绍和平台规划》&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-Appium-inspector-iOS-simulator" rel="nofollow" target="_blank" title=""&gt;《从 0 到 1 搭建移动 App 功能自动化测试平台（1）：模拟器中运行 iOS 应用》&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>debugtalk</author>
      <pubDate>Sun, 29 May 2016 19:49:49 +0800</pubDate>
      <link>https://ruby-china.org/topics/30154</link>
      <guid>https://ruby-china.org/topics/30154</guid>
    </item>
    <item>
      <title>从 0 到 1 搭建移动 App 功能自动化测试平台 (1)：模拟器中运行 iOS 应用</title>
      <description>&lt;p&gt;在&lt;a href="http://mp.weixin.qq.com/s?__biz=MzAxMTczMjgzMQ==&amp;amp;mid=2650587826&amp;amp;idx=1&amp;amp;sn=dc7bb80cb0a5864fc29a8615d77840f4#rd" rel="nofollow" target="_blank" title=""&gt;上一篇文章&lt;/a&gt;中，我对本系列教程的项目背景进行了介绍，并对自动化测试平台的建设进行了规划。&lt;/p&gt;

&lt;p&gt;在本文中，我将在已准备就绪的 iOS 自动化测试环境的基础上，通过 Appium 调用模拟器运行 iOS 应用。内容很是基础，熟悉的同学可直接略过。&lt;/p&gt;
&lt;h2 id="iOS应用安装包的基础知识"&gt;iOS 应用安装包的基础知识&lt;/h2&gt;
&lt;p&gt;作为完全的 iOS 新手，困惑的第一个问题就是 iOS 安装包文件。&lt;/p&gt;

&lt;p&gt;在 Android 系统中，安装 App 的途径很多，除了各类应用市场，普通用户也经常直接下载 apk 安装包文件后手动进行安装，因此大家对 Android 的安装包文件都比较熟悉。&lt;/p&gt;

&lt;p&gt;但是对于 iOS 系统就不一样了，由于我们普通用户在 iOS 上安装应用的时候基本上只能通过 Apple Store 进行安装（未越狱），没有机会接触原始的安装包文件，因此往往连 iOS 应用的安装包到底是什么格式后缀都不清楚。&lt;/p&gt;

&lt;p&gt;现在我们想在 Appium App 中通过模拟器运行被测应用，需要指定 iOS app 的安装包路径，因此需要首先获得一个 iOS app 安装包。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/Appium_iOS_Settings_init.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;那么 iOS app 的安装包长啥样呢？&lt;/p&gt;

&lt;p&gt;或者在这个问题之前，我们先来看下另一个问题：对于 iOS 设备来说，如果不通过 Apple Store，我们可以怎样安装一个应用？&lt;/p&gt;

&lt;p&gt;针对这个问题，我搜了些资料，也请教了周围的同事，了解到的途径有如下几个：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;企业证书：该种方式适用于企业内部；通过企业证书编译出的 iOS 应用，无需上传至 Apple Store，即可无限制的安装到企业员工的 iOS 设备中。只是需要解决的一个问题是，由于 iOS 设备没有文件管理器，没法将安装包拷贝到 iOS 设备中，因此常用的做法是将安装包（&lt;code&gt;.ipa&lt;/code&gt;文件）上传至一些下载服务器（例如&lt;code&gt;fir.im&lt;/code&gt;），并生成二维码，然后用户扫描二维码后即可通过浏览器下载安装包并进行安装。由此联想到另外一个方法，通过微信文件传输助手将安装包（&lt;code&gt;.ipa&lt;/code&gt;）传输至 iOS 设备，然后再进行安装应该也是可以的吧？这种方法不知在原理上是否可行，因为在试验时由于安装包大于 30M，微信无法传输，所以没能进行验证。&lt;/li&gt;
&lt;li&gt;Xcode：该种方式适用于 iOS 开发者；开发者在 Xcode 中连上 iOS 设备对源码进行编译，编译生成的应用会自动安装至 iOS 设备。当然，该种方式也是需要 iOS 开发者证书。&lt;/li&gt;
&lt;li&gt;PP 助手：该种方式适用于普通用户；PP 助手是一个非苹果官方的设备资源管理工具，可以实现对未越狱的 iOS 设备进行应用管理，也可以安装本地&lt;code&gt;.ipa&lt;/code&gt;文件，前提是&lt;code&gt;.ipa&lt;/code&gt;文件具有合适的签名。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在上面列举的安装应用的途径中，反复提到了&lt;code&gt;.ipa&lt;/code&gt;文件，那&lt;code&gt;.ipa&lt;/code&gt;应该就是 iOS 应用程序的后缀了吧？暂且这么认为吧。&lt;/p&gt;

&lt;p&gt;再回到前面的场景，要在 iOS 模拟器中运行 iOS 应用，我们是否可以找研发人员要一个&lt;code&gt;.ipa&lt;/code&gt;安装包文件，然后就能在模拟器中加载运行应用呢？&lt;/p&gt;

&lt;p&gt;刚开始的时候我是这么认为的。于是我获取到&lt;code&gt;.ipa&lt;/code&gt;文件后，在&lt;code&gt;App Path&lt;/code&gt;中填写该文件的路径，然后启动 Appium Server；接着我再打开 Inspector 时，发现 iOS 模拟器启动了，但是在应用启动的时候就出问题了，始终无法正常启动，感觉像是启动崩溃，反复尝试多次仍然如此。&lt;/p&gt;

&lt;p&gt;再次经过 Google，总算是明白出现问题的原因了，总结下来有如下几点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;不管是从 Apple Store 或 iTunes 上下载的应用，还是在 Xcode 中针对真机设备编译生成的&lt;code&gt;.ipa&lt;/code&gt;文件，都是面向于 ARM 处理器的 iOS 设备，只能在真机设备中进行安装；&lt;/li&gt;
&lt;li&gt;而在 Mac OSX 系统中运行的 iOS 模拟器，运行环境是基于 Intel 处理器的；&lt;/li&gt;
&lt;li&gt;因此，若是针对真机设备编译生成的&lt;code&gt;.ipa&lt;/code&gt;文件，是无法在 iOS 模拟器中正常运行的，毕竟处理器架构都不一样；&lt;/li&gt;
&lt;li&gt;要想在 iOS 模拟器中运行应用，则必须在 Xcode 中编译时选择模拟器类型；编译生成的文件后缀为&lt;code&gt;.app&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="准备.app文件"&gt;准备&lt;code&gt;.app&lt;/code&gt;文件&lt;/h2&gt;
&lt;p&gt;接下来，就说下如何获取&lt;code&gt;.app&lt;/code&gt;文件。&lt;/p&gt;

&lt;p&gt;虽然是测试人员，不会对被测 iOS 项目贡献代码，但是也不能总是找研发帮忙编译生成&lt;code&gt;.app&lt;/code&gt;文件。所以，在本地搭建完整的 iOS 项目开发环境还是很有必要的。&lt;/p&gt;

&lt;p&gt;对于 iOS 开发环境的搭建，当前社区中应该已经有了很多完整的教程，我在这儿就不详细描述了，只简单说下我搭建过程中涉及到的几个点。&lt;/p&gt;

&lt;p&gt;首先，Mac OSX、Xcode、Apple Developer Tools 这些基础环境的安装，在上一篇文章中已经进行说明了；&lt;/p&gt;

&lt;p&gt;然后，申请项目源码的访问权限，&lt;code&gt;git clone&lt;/code&gt;到本地；&lt;/p&gt;

&lt;p&gt;接着是项目依赖环境的问题；通常一个较大型的 iOS 项目都会引用许多第三方库，而这些依赖库并不会直接保存到项目仓库中，通常是采用&lt;code&gt;CocoaPods&lt;/code&gt;进行管理；简单地说，&lt;code&gt;CocoaPods&lt;/code&gt;是针对&lt;code&gt;Swift&lt;/code&gt;和&lt;code&gt;Objective-C&lt;/code&gt;项目的依赖管理器，类似于 Java 中的&lt;code&gt;Maven&lt;/code&gt;，Ruby 中的&lt;code&gt;Gem&lt;/code&gt;，Python 中的&lt;code&gt;pip&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;当然，iOS 项目的依赖管理工具也不是只有&lt;code&gt;CocoaPods&lt;/code&gt;一个，如果是采用的别的依赖管理器，请自行查找对应的资料。&lt;/p&gt;

&lt;p&gt;采用&lt;code&gt;CocoaPods&lt;/code&gt;管理的项目，在项目根目录下会包含&lt;code&gt;Podfile&lt;/code&gt;和&lt;code&gt;Podfile.lock&lt;/code&gt;文件，里面记录了当前项目依赖的第三方库以及对应的版本号。&lt;/p&gt;

&lt;p&gt;安装&lt;code&gt;CocoaPods&lt;/code&gt;很简单，采用&lt;code&gt;gem&lt;/code&gt;即可。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;cocoapods
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，进入到 iOS 项目的目录，执行&lt;code&gt;pod install&lt;/code&gt;命令即可安装当前项目的所有依赖。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;Project_Folder
&lt;span class="nv"&gt;$ &lt;/span&gt;pod &lt;span class="nb"&gt;install
&lt;/span&gt;Re-creating CocoaPods due to major version update.
Analyzing dependencies
.....（略）
Downloading dependencies
.....（略）
Generating Pods project
Integrating client project
Sending stats
Pod installation &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; There are 27 dependencies from the Podfile and 28 total pods installed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于&lt;code&gt;CocoaPods&lt;/code&gt;的更多信息，请自行查看&lt;a href="https://cocoapods.org" rel="nofollow" target="_blank" title=""&gt;官方网站&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;在依赖安装完成后，正常情况下，就可以在 Xcode 中编译项目了。&lt;/p&gt;

&lt;p&gt;没有别的需要注意的，将 target 选择为模拟器（iOS Simulator）即可。而且针对模拟器进行编译时，也不会涉及到开发者证书的问题，项目配置上会简单很多。待后续讲到真机上的自动化测试时，我再对证书方面的内容进行补充。&lt;/p&gt;

&lt;p&gt;编译完成后，在 Products 目录下，就可以看到&lt;code&gt;XXX.app&lt;/code&gt;文件，这里的&lt;code&gt;XXX&lt;/code&gt;就是项目名称；然后，选中&lt;code&gt;XXX.app&lt;/code&gt;文件，【Show in Finder】，即可在文件目录中定位到该文件。&lt;/p&gt;

&lt;p&gt;接下来，将&lt;code&gt;XXX.app&lt;/code&gt;文件拷贝出来，或者复制该文件的&lt;code&gt;Full path&lt;/code&gt;，怎样都行，只要在&lt;code&gt;Appium&lt;/code&gt;的&lt;code&gt;App Path&lt;/code&gt;中能定位到该文件就行。&lt;/p&gt;
&lt;h2 id="模拟器中运行iOS应用"&gt;模拟器中运行 iOS 应用&lt;/h2&gt;
&lt;p&gt;被测应用&lt;code&gt;.app&lt;/code&gt;准备就绪后，接下来就可以在 iOS 模拟器中运行了。&lt;/p&gt;

&lt;p&gt;回到前面的那张图。启动&lt;code&gt;Appium app&lt;/code&gt;后，对于模拟器运行的情况，在&lt;code&gt;iOS Settings&lt;/code&gt;中必须设置的参数项就 3 个，&lt;code&gt;App Path&lt;/code&gt;、&lt;code&gt;Force Device&lt;/code&gt;和&lt;code&gt;Platform Version&lt;/code&gt;。对于真机运行的情况，后续再单独进行说明。&lt;/p&gt;

&lt;p&gt;设置完毕后，点击【Launch】，启动&lt;code&gt;Appium Server&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/Appium_Inspector_Button.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;然后，点击图中红框处的按钮，即可通过&lt;code&gt;Inspector&lt;/code&gt;启动模拟器，并在模拟器中加载 iOS 应用。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/Appium_iOS_Simulator_Console.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;在模拟器中，我们可以像在真机中一样，体验被测应用的各项功能；并且，在 Appium 的日志台中，可以实时查看到日志信息。&lt;/p&gt;
&lt;h2 id="经历的一个坑"&gt;经历的一个坑&lt;/h2&gt;
&lt;p&gt;整个过程是挺简单的，不过，在探索过程中我还是有遇到一个坑。&lt;/p&gt;

&lt;p&gt;通过&lt;code&gt;Inspector&lt;/code&gt;启动模拟器时，总是弹框报错，报错形式如下。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/Appium_Inspector_Error.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;刚开始出现这问题时百思不得其解，因为提示的信息并不明显，Google 了好一阵也没找到原因。最后只有详细去看日志信息，才发现问题所在。&lt;/p&gt;

&lt;p&gt;在日志中，发现的报错信息如下：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[iOS] Error: Could not find a device to launch. You requested 'iPhone 6 (8.4)', but the available devices were: ["Apple TV 1080p (9.2) [98638D25-7C82-48DF-BDCA-7F682F951533] (Simulator)","iPad 2 (9.2) [5E22F53E-EAB3-45DF-A1DD-10F58E920679] (Simulator)","iPad 2 (9.3) [4B2D2F9A-C099-4C13-8DE9-27C826A521C2] (Simulator)","iPad Air (9.2) [825E4997-9CD8-4225-9977-4C7AE2C98389] (Simulator)","iPad Air (9.3) [E4523799-E35F-4499-832B-12CF33F09144] (Simulator)","iPad Air 2 (9.2) [8057039D-F848-453E-97EC-2F75CAEA2E77] (Simulator)","iPad Air 2 (9.3) [0B8F49DA-832A-4248-BA1D-9DA5D11E31FD] (Simulator)","iPad Pro (9.2) [AF1F2D06-3067-41B5-AC2B-4B0ED88BF5D9] (Simulator)","iPad Pro (9.3) [C39617A6-9D91-4C0B-B25B-741BD57B016C] (Simulator)","iPad Retina (9.2) [D3C694E1-E3B4-47BE-AB5E-80B3D4E22FC2] (Simulator)","iPad Retina (9.3) [907C7B06-ED2C-48AC-AC46-04E4AD6E0CA3] (Simulator)","iPhone 4s (9.2) [1A786195-94E3-4908-8309-7B66D84E4619] (Simulator)","iPhone 4s (9.3) [3F76F34B-5A8F-4FD1-928D-56F84C192DDD] (Simulator)","iPhone 5 (9.2) [0D79A4CA-71EB-48A6-9EE4-172BEF3EB4E0] (Simulator)","iPhone 5 (9.3) [04270D44-F831-4253-95F2-3D205D2BC0D9] (Simulator)","iPhone 5s (9.2) [13A16C07-3C5B-4B04-A94B-B40A63238958] (Simulator)","iPhone 5s (9.3) [D30A7B34-BA01-4203-80DA-FAEA436725F9] (Simulator)","iPhone 6 (9.2) [5D01650F-2A31-4D53-A47A-CCF7FD552ADD] (Simulator)","iPhone 6 (9.3) [2F0810F6-C73B-4BA4-93BA-06D4B6D96BDA] (Simulator)","iPhone 6 Plus (9.2) [9A840B78-E6CE-4D18-BE83-16B590411641] (Simulator)","iPhone 6 Plus (9.3) [27C6557A-B09D-4D8A-9846-DA8FE0A8E8D5] (Simulator)","iPhone 6s (9.2) [E7F5B8A5-0E85-404F-A4D4-191D63E7EC1B] (Simulator)","iPhone 6s (9.3) [6F702911-13C2-472C-9ECD-BADD4385CB77] (Simulator)","iPhone 6s (9.3) + Apple Watch - 38mm (2.2) [B63FFAA4-00A4-473B-9462-3664F41F9001] (Simulator)","iPhone 6s Plus (9.2) [58837F78-511A-4F0B-9DDF-782E3B9935BD] (Simulator)","iPhone 6s Plus (9.3) [C31003C6-DCE2-414D-AD7F-376F6FA995B0] (Simulator)","iPhone 6s Plus (9.3) + Apple Watch - 42mm (2.2) [E3154768-CA23-45CC-90E5-2D0386A57B7D] (Simulator)"]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题在于，我设置&lt;code&gt;iOS Settings&lt;/code&gt;时，将&lt;code&gt;Force Device&lt;/code&gt;设置为"iPhone 6"，将&lt;code&gt;Platform Version&lt;/code&gt;设置为“8.4”，但是经过组合，&lt;code&gt;iPhone 6 (8.4)&lt;/code&gt;并不在可用的模拟器设备列表中。&lt;/p&gt;

&lt;p&gt;再来看日志中提示的可用设备，发现“iPhone 6”设备对应的&lt;code&gt;Platform Version&lt;/code&gt;只有“9.2”和“9.3”。然后回到&lt;code&gt;iOS Settings&lt;/code&gt;，发现&lt;code&gt;Platform Version&lt;/code&gt;的下拉框可选项就没有“9.2”和“9.3”，最新的一个可选版本也就是“8.4”。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/Appium_iOS_Settings_bug.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;这应该是&lt;code&gt;Appium app&lt;/code&gt;的一个 bug 吧。不过好在&lt;code&gt;Platform Version&lt;/code&gt;参数虽然是通过下拉框选择，但是也可以在框内直接填写内容。于是我在&lt;code&gt;Platform Version&lt;/code&gt;设置框内填写为“9.3”，然后再次启动时，发现 iOS 模拟器就可以正常启动了。&lt;/p&gt;
&lt;h2 id="To be continued ..."&gt;To be continued ...&lt;/h2&gt;
&lt;p&gt;现在，我们已经成功地通过 Appium Inspector 调用模拟器并运行 iOS 应用，接下来，我们就要开始尝试编写自动化测试用例了。&lt;/p&gt;

&lt;p&gt;在下一篇文章中，我们将对 Appium Inspector 的功能进行熟悉，通过 Inspector 来查看 iOS 应用的 UI 元素信息，并尝试采用脚本语言与 UI 进行交互操作。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="Read More ..."&gt;Read More ...&lt;/h2&gt;
&lt;p&gt;公众号：&lt;a href="http://debugtalk.com/assets/images/wechat_qrcode.png" rel="nofollow" target="_blank" title=""&gt;DebugTalk&lt;/a&gt;
原文链接：&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-Appium-inspector-iOS-simulator" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-Appium-inspector-iOS-simulator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/wechat_qrcode.png" title="" alt="DebugTalk"&gt;&lt;/p&gt;
&lt;h2 id="相关文章"&gt;相关文章&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-backgroud-introduction" rel="nofollow" target="_blank" title=""&gt;《从 0 到 1 搭建移动 App 功能自动化测试平台（0）背景介绍和平台规划》&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>debugtalk</author>
      <pubDate>Sat, 21 May 2016 19:27:35 +0800</pubDate>
      <link>https://ruby-china.org/topics/30085</link>
      <guid>https://ruby-china.org/topics/30085</guid>
    </item>
    <item>
      <title>从 0 到 1 搭建移动 App 功能自动化测试平台：(0) 背景介绍和平台规划</title>
      <description>&lt;h2 id="背景"&gt;背景&lt;/h2&gt;
&lt;p&gt;最近新加入 DJI 的某项目组（以下均已 M 指代），需要从零开始搭建功能自动化测试平台。&lt;/p&gt;

&lt;p&gt;简单地说，M 是一个典型的移动互联网产品，客户端包括 iOS 和 Android，并在 app 中通过 WebView 嵌入了 H5，后端基于 Ruby on Rails 实现。&lt;/p&gt;

&lt;p&gt;当前阶段，M 项目除了 Rails Server 端采用 Jenkins+RSpec 实现了部分的持续集成功能外，客户端部分的部署和测试工作都还是完全依赖于手工操作。&lt;/p&gt;

&lt;p&gt;基于当前项目的开发模式，我对整个 M 项目实现持续集成自动化测试的架构流程进行了规划，初步计划的架构图如下图所示。最终的目标是希望能实现：不管是 Rails Server，还是 App(iOS/Android)，以及 H5，当任意部分存在代码提交时，系统能自动拉取最新代码进行部署并执行自动化回归测试，及时地将执行情况反馈给开发人员。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/DJI_Plus_Automated_Test_Platform.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;目标确定后，便是分阶段进行实现，需要开发的模块包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;自动化测试平台（Automated Test Platform）：满足 iOS/Android/H5 的自动化功能测试，包括模拟器和真机的测试；&lt;/li&gt;
&lt;li&gt;测试管理平台（Test Management Platform）：实现自动化测试用例管理、手动下发测试任务、测试结果报表展现、Dashboard 等功能；&lt;/li&gt;
&lt;li&gt;打包平台（Pack System）：实现 iOS/Android 的自动化构建；&lt;/li&gt;
&lt;li&gt;服务端自动化测试（Rails）：将服务端 Rails 的自动化测试接入测试管理平台；&lt;/li&gt;
&lt;li&gt;持续集成流程打通：对 Jenkins 进行二次开发，与测试管理平台打通，实现全流程的持续集成自动化测试。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;而本系列教程，《从 0 到 1 搭建移动 App 功能自动化测试平台》，便是对整个实践过程的一个记录。&lt;/p&gt;

&lt;p&gt;需要说明的是，之前我个人的工作经历主要在服务端性能测试、Android 客户端性能测试（测试开发）方向，对于客户端的自动化测试基本上没有经验积累，特别是 iOS 系统的测试，以前更是完全没有接触过。因此本系列教程只能算是个人在探索路上的学习总结和记录，可能会存在一些错误的观点，还请前辈们多多指教。&lt;/p&gt;
&lt;h2 id="自动化测试框架的选择"&gt;自动化测试框架的选择&lt;/h2&gt;
&lt;p&gt;在愿景图中，绿色方框（Automated Test Platform）负责移动应用客户端（iOS/Android/H5）自动化测试的调度和执行，是整个自动化测试平台的核心。&lt;/p&gt;

&lt;p&gt;因此，在搭建自动化测试平台之前，首先需要选择一个合适的自动化测试框架。&lt;/p&gt;

&lt;p&gt;对于移动应用的自动化测试框架，当前市面上已经有很多成熟的开源项目。针对当前项目的实际情况，我主要参考如下选择标准：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;同时支持 iOS、Android、H5，且尽量能保持接口统一，减少开发维护成本；&lt;/li&gt;
&lt;li&gt;编程语言支持 Python/Ruby；&lt;/li&gt;
&lt;li&gt;用户量大，文档丰富。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;经过筛选，Appium 无疑是最佳的选择。&lt;/p&gt;
&lt;h2 id="Appium简介"&gt;Appium 简介&lt;/h2&gt;
&lt;p&gt;对于 Appium 的详细介绍，大家可参考&lt;a href="http://appium.io/" rel="nofollow" target="_blank" title=""&gt;Appium&lt;/a&gt;官方文档，我就不再重复引用。&lt;/p&gt;

&lt;p&gt;不过对于 Appium，仍然有几点很赞的理念值得强调。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;采用 Appium 时，无需对被测应用做任何修改，也无需嵌入任何东西；&lt;/li&gt;
&lt;li&gt;Appium 对 iOS 和 Android 的原生自动化测试框架进行了封装，并提供了统一的 API（WebDriver API），减少了自动化测试代码的维护工作量；&lt;/li&gt;
&lt;li&gt;Appium 采用 Client-Server 的架构设计，并采用标准的 HTTP 通信协议；Server 端负责与 iOS/Android 原生测试框架交互，无需测试人员关注细节实现；Client 端基本上可以采用任意主流编程语言编写测试用例，减少了学习成本。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="环境准备（iOS）"&gt;环境准备（iOS）&lt;/h2&gt;
&lt;p&gt;在 Appium 中测试 iOS 时，依赖于 Apple 开发环境，因此，在运行 Appium 之前需要先确保如下环境安装正确。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mac OS X &amp;gt;= 10.7&lt;/li&gt;
&lt;li&gt;XCode &amp;gt;= 4.6.3&lt;/li&gt;
&lt;li&gt;Apple Developer Tools (iPhone simulator SDK, command line tools)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如上几个环境安装比较简单，直接在 Apple Store 中安装即可。&lt;/p&gt;

&lt;p&gt;在安装 Appium 之前，为了确保 Appium 的相关依赖已经准备就绪，可以使用&lt;code&gt;appium-doctor&lt;/code&gt;来进行验证。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/appium/appium-doctor" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;appium-doctor&lt;/code&gt;&lt;/a&gt;是一个用于验证 appium 安装环境的工具，可以诊断出&lt;code&gt;Node/iOS/Android&lt;/code&gt;环境配置方面的常见问题。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;appium-doctor&lt;/code&gt;采用&lt;code&gt;node.js&lt;/code&gt;编写，采用&lt;code&gt;npm&lt;/code&gt;即可在 Terminal 中进行安装：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;appium-doctor &lt;span class="nt"&gt;-g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完毕后，执行&lt;code&gt;appium-doctor&lt;/code&gt;命令即可对&lt;code&gt;Appium&lt;/code&gt;的环境依赖情况进行检测；指定&lt;code&gt;--ios&lt;/code&gt;时只针对 iOS 环境配置进行检测，指定&lt;code&gt;--android&lt;/code&gt;参数时只针对 Android 环境配置进行检测，若不指定则同时对 iOS 和 Android 环境进行检测。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;appium-doctor &lt;span class="nt"&gt;--ios&lt;/span&gt;                                                                                                                               
info AppiumDoctor &lt;span class="c"&gt;### Diagnostic starting ###&lt;/span&gt;
info AppiumDoctor  ✔ Xcode is installed at: /Applications/Xcode.app/Contents/Developer
info AppiumDoctor  ✔ Xcode Command Line Tools are installed.
info AppiumDoctor  ✔ DevToolsSecurity is enabled.
info AppiumDoctor  ✔ The Authorization DB is &lt;span class="nb"&gt;set &lt;/span&gt;up properly.
info AppiumDoctor  ✔ The Node.js binary was found at: /usr/local/bin/node
info AppiumDoctor  ✔ HOME is &lt;span class="nb"&gt;set &lt;/span&gt;to: /Users/Leo
info AppiumDoctor &lt;span class="c"&gt;### Diagnostic completed, no fix needed. ###&lt;/span&gt;
info AppiumDoctor 
info AppiumDoctor Everything looks good, bye!
info AppiumDoctor 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若检测结果全部通过，则说明 Appium 的相关依赖已经准备就绪，接下来可以继续安装 Appium。&lt;/p&gt;
&lt;h2 id="安装Appium"&gt;安装 Appium&lt;/h2&gt;
&lt;p&gt;根据前面的介绍，Appium 采用 Client-Server 的架构设计，因此安装 Appium 时需要分别安装 Server 部分和 Client 部分。&lt;/p&gt;

&lt;p&gt;通常情况下，我们说的 Appium 都是指代的 Server 部分。Appium 的安装有多种方式：可以通过源码编译安装，也可以在 Terminal 中通过&lt;code&gt;npm&lt;/code&gt;命令安装，另一种是直接下载&lt;a href="https://github.com/appium/appium/releases" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;appium.dmg&lt;/code&gt;&lt;/a&gt;后安装应用程序。&lt;/p&gt;

&lt;p&gt;在这里推荐运行&lt;code&gt;Appium app&lt;/code&gt;的方式，除了 GUI 界面操作更直观以外，更重要的一个原因是，相比于命令行运行方式，&lt;code&gt;Appium app&lt;/code&gt;多了一个&lt;code&gt;Inspector&lt;/code&gt;模块，可以调用模拟器运行被测应用程序，并且可以很方便地在预览页面中查看 UI 元素的层级结构和详细控件属性，极大地提高编写测试脚本的效率。&lt;/p&gt;

&lt;p&gt;至于 Client 部分，其实我们原本可以不安装任何东西，只需要任意选择一门开发语言，然后直接基于 WebDriver 的 C/S 协议（JSON Wire Protocol）即可编写自动化测试代码。但是这样做的话工作量会比较大，因为要去处理一些跟协议相关的工作。所幸 Appium 项目已经针对众多主流的编程语言，将底层协议处理相关的工作封装为 Library，通过调用这些 Library，可以极大地简化我们编写测试用例的工作量。&lt;/p&gt;

&lt;p&gt;而说的需要安装的 Client 部分，其实也就是安装这些 Library。选定编写测试用例的语言后，我们就可以针对性地进行安装。&lt;/p&gt;

&lt;p&gt;例如，如果选择 Ruby 语言，那么需要安装的 Library 就是&lt;code&gt;appium_lib&lt;/code&gt;，安装方式如下：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;appium_lib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果选择 Python 语言，那么需要安装的 Library 就是&lt;code&gt;Appium-Python-Client&lt;/code&gt;，安装方式如下：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;Appium-Python-Client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于其它编程语言，请自行参考官方文档。&lt;/p&gt;
&lt;h2 id="To be continued ..."&gt;To be continued ...&lt;/h2&gt;
&lt;p&gt;iOS 的自动化测试环境已基本准备就绪了，接下来我们想做的第一件事，就是在模拟器中运行 iOS 应用。&lt;/p&gt;

&lt;p&gt;在下一篇文章中，我们将从 clone 项目源码为起点，编译生成 iOS app，在 Appium 中调用模拟器中运行 iOS app，并分享实践过程中遇到的一些坑。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="Read More ..."&gt;Read More ...&lt;/h2&gt;
&lt;p&gt;公众号：&lt;a href="http://debugtalk.com/assets/images/wechat_qrcode.png" rel="nofollow" target="_blank" title=""&gt;DebugTalk&lt;/a&gt;
原文链接：&lt;a href="http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-backgroud-introduction" rel="nofollow" target="_blank"&gt;http://debugtalk.com/post/build-app-automated-test-platform-from-0-to-1-backgroud-introduction&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/wechat_qrcode.png" title="" alt="DebugTalk"&gt;&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Fri, 20 May 2016 12:24:41 +0800</pubDate>
      <link>https://ruby-china.org/topics/30074</link>
      <guid>https://ruby-china.org/topics/30074</guid>
    </item>
    <item>
      <title>Android App 持续集成性能测试：启动流量 (2)</title>
      <description>&lt;p&gt;在&lt;a href="https://testerhome.com/topics/4783" rel="nofollow" target="_blank" title=""&gt;上一篇文章&lt;/a&gt;中，介绍了一种测试 Android App 启动流量的方法。当时也提到了，通过读取&lt;code&gt;/proc/uid_stat/&amp;lt;UID&amp;gt;/&lt;/code&gt;目录下的&lt;code&gt;tcp_rcv&lt;/code&gt;和&lt;code&gt;tcp_snd&lt;/code&gt;文件，只能得到 App 的流量总值，无法得到更细化的数据。&lt;/p&gt;

&lt;p&gt;例如，UC 浏览器国际版在启动后，会和美国的服务器进行通讯交互，如果我们想测试浏览器在启动后与美国服务器的通讯流量，要怎么实现呢？。&lt;/p&gt;

&lt;p&gt;本文便是针对这类场景的测试需求，讲解如何采用&lt;code&gt;tcpdump&lt;/code&gt;测试得到更细化的流量数据。&lt;/p&gt;
&lt;h2 id="tcpdump"&gt;tcpdump&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tcpdump&lt;/code&gt;，是一个在 Unix-like 系统中通用的网络抓包工具，当然，这个工具在 Android 系统中也是可以使用的。&lt;/p&gt;

&lt;p&gt;对于工具本身，本文不做过多介绍。为了防止有读者之前完全没有&lt;code&gt;tcpdump&lt;/code&gt;的使用经验，在这里我只简单地进行几点说明：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;大多 Android 系统默认未集成 tcpdump 工具，我们需要事先将专门针对 Android 系统编译好的的 tcpdump 导入到 Android 系统，例如&lt;code&gt;/data/local/tmp/tcpdump&lt;/code&gt;；当然，我们也不用自己编译，在&lt;a href="http://www.androidtcpdump.com" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;androidtcpdump&lt;/code&gt;&lt;/a&gt;网站就可以下载到编译好的 tcpdump 二进制文件。&lt;/li&gt;
&lt;li&gt;运行&lt;code&gt;tcpdump&lt;/code&gt;工具时需要 root 权限。&lt;/li&gt;
&lt;li&gt;tcpdump 命令支持许多参数，常见的有：

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-i&lt;/code&gt;指定网卡（interface），&lt;code&gt;any&lt;/code&gt;表示不限网卡；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-c&lt;/code&gt;指定接收的 packets 数量，接收完成后自动停止抓包；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-w&lt;/code&gt;指定输出文件，输出文件的格式为 pcap；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-s&lt;/code&gt;(&lt;code&gt;--snapshot-length&lt;/code&gt;) 指定在每个 packet 中最多截取的字节数，设置为 0 时表示截取上限取默认值 262144；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-v&lt;/code&gt;/&lt;code&gt;-vv&lt;/code&gt;/&lt;code&gt;-vvv&lt;/code&gt;，指定输出的详细程度，针对流量测试，我们不需要非常详尽的输出数据，取&lt;code&gt;-v&lt;/code&gt;即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;基于以上对&lt;code&gt;tcpdump&lt;/code&gt;的介绍，我们要测试浏览器在启动后与美国服务器的通讯流量，就只需要先启动浏览器，然后在&lt;code&gt;adb shell&lt;/code&gt;中执行以下命令即可。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;1|shell@hammerhead:/ &lt;span class="nv"&gt;$ &lt;/span&gt;su &lt;span class="nt"&gt;-c&lt;/span&gt; /data/local/tmp/tcpdump &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; any &lt;span class="nt"&gt;-s&lt;/span&gt; 0 &lt;span class="nt"&gt;-c&lt;/span&gt; 2000 &lt;span class="nt"&gt;-w&lt;/span&gt; /sdcard/us.pcap
tcpdump: listening on any, link-type LINUX_SLL &lt;span class="o"&gt;(&lt;/span&gt;Linux cooked&lt;span class="o"&gt;)&lt;/span&gt;, capture size 262144 bytes
2000 packets captured
2024 packets received by filter
0 packets dropped by kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里之所有指定接收 packets 数为 2000，是因为浏览器启动后并不是立即与美国服务器进行通讯。所以在这里设置了一个比较大的值，确保浏览器与美国服务器的异步通讯数据能包含在这 2000packets 之中。当然，这个 2000 只是一个工程实践得到的经验值，具体取多少还是要依赖于具体场景。&lt;/p&gt;

&lt;p&gt;经过一段时间的抓包后，就生产了抓包结果，即&lt;code&gt;/sdcard/us.pcap&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="人工分析pcap文件"&gt;人工分析 pcap 文件&lt;/h2&gt;
&lt;p&gt;拿到 pcap 文件只是第一步，我们必须要对这个文件进行解析才能得到我们想要的结果。&lt;/p&gt;

&lt;p&gt;那么，通过什么方法解析 pcap 文件呢？&lt;/p&gt;

&lt;p&gt;先简单介绍下 pcap。pcap，即&lt;code&gt;packet capture&lt;/code&gt;的缩写，是一种通用的网络抓包数据存储格式。既然是通用，因此它除了可以被&lt;code&gt;tcpdump&lt;/code&gt;解析外，还支持被多种网络工具解析，其中，就包括大家熟知的&lt;code&gt;wireshark&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;至于为什么有了&lt;code&gt;tcpdump&lt;/code&gt;还要用&lt;code&gt;wireshark&lt;/code&gt;来解析，这主要还是因为&lt;code&gt;wireshark&lt;/code&gt;是图形界面，操作和使用上更友好一些。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;wireshark&lt;/code&gt;中分析 pcap 文件十分简单，只需要直接打开文件，就可以看到抓包过程中捕获的所有网络通讯数据。不过，由于我们抓包获得的数据是系统层面的，除了我们关注的与美国服务器的通讯交互外，还包含了非常多的其它通讯信息。&lt;/p&gt;

&lt;p&gt;好在&lt;code&gt;wireshark&lt;/code&gt;有非常强大的筛选功能。对于本案例，我们可以先确定出美国服务器的 host 或 IP，例如 host 为&lt;code&gt;ucus.ucweb.com&lt;/code&gt;，那么我们就可以在筛选器中通过&lt;code&gt;http.host == "ucus.ucweb.com"&lt;/code&gt;语句，即可筛选出所有本地与美国服务器的通讯交互数据。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/wireshark_host_filter.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;对于更丰富的筛选功能，大家可以根据实际需求查询&lt;code&gt;wireshark&lt;/code&gt;的帮助文档，在此就不再进行展开。&lt;/p&gt;

&lt;p&gt;从上图的筛选结果中可以看到，美国服务器的地址为&lt;code&gt;168.235.199.134&lt;/code&gt;。那接下来如何查看通讯的流量大小呢？&lt;/p&gt;

&lt;p&gt;首先，找出该次请求的&lt;code&gt;TCP Stream&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/wireshark_tcp_stream_menu.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;在筛选出的&lt;code&gt;TCP Stream&lt;/code&gt;中，将各条记录的 Length 进行求和，即可得到总的大小。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/wireshark_tcp_stream_data.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;例如，发送流量的总和，即&lt;code&gt;100.84.126.160&lt;/code&gt;-&amp;gt;&lt;code&gt;168.235.199.134&lt;/code&gt;的总和，加和总值为 3722bytes；接收流量的总和，即&lt;code&gt;168.235.199.134&lt;/code&gt;-&amp;gt;&lt;code&gt;100.84.126.160&lt;/code&gt;的总和，加和总值为 6300bytes。&lt;/p&gt;

&lt;p&gt;当然，这里只是为了讲解计算流量的原理，实际上，我们并不需要去进行这个计算，可以直接读取得到总值。&lt;/p&gt;

&lt;p&gt;【Statistics】-&amp;gt;【Endpoints】&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/wireshark_endpoints_menu.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;在 Endpoints 界面中，选择&lt;code&gt;TCP&lt;/code&gt; tab，勾选“Limit to display filter”，即可看到通讯流量汇总数据。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://debugtalk.com/assets/images/wireshark_tcp_stream_data.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;可以看出，这个的汇总数值与前面计算得到的数值完全相同。&lt;/p&gt;
&lt;h2 id="自动化测试脚本"&gt;自动化测试脚本&lt;/h2&gt;
&lt;p&gt;通过前面的人工分析，我们已经掌握了分析特定流量的一般性方法。然而，要想将此种场景的流量测试加入持续集成自动化测试系统，采用以上方法显然是不行的。&lt;/p&gt;

&lt;p&gt;那么，要怎样才能在代码中完成对 pcap 文件的分析呢？&lt;/p&gt;

&lt;p&gt;好在已经有前辈做了相应的工作，在 GitHub 上就找到了一个开源项目&lt;a href="https://github.com/andrewf/pcap2har" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;pcap2har&lt;/code&gt;&lt;/a&gt;，可以实现对 pcap 文件的解析。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pcap2har&lt;/code&gt;项目的详细介绍请大家自行查看项目文档。&lt;/p&gt;

&lt;p&gt;针对本文中的测试场景，解析 pcap 文件的代码实现如下。&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python
#coding=utf-8
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dpkt&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pcap2har&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pcap&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pcap2har&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parsePcapFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcap_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_host&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# parse pcap file
&lt;/span&gt;    &lt;span class="n"&gt;dispatcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pcap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EasyParsePcap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pcap_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;traffic_total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;traffic_receive_total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;traffic_send_total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;url_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# stream为tcp数据流，当为长链接时一个tcp流里面可以有多个http请求
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# fwd为请求大小，如果小于200则肯定不是正常的HTTP请求，忽略
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fwd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;caplen&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;pointer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;pointer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fwd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;myrequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;#解析请求头
&lt;/span&gt;            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;dpkt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# if the message failed
&lt;/span&gt;                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;

            &lt;span class="n"&gt;pointer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;myrequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data_consumed&lt;/span&gt;
            &lt;span class="n"&gt;myhead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;myrequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;

            &lt;span class="c1"&gt;# 请求头大小&amp;lt;200时忽略
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;myrequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data_consumed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;myhead&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;target_host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;traffic_receive_total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;caplen&lt;/span&gt;
                &lt;span class="n"&gt;traffic_send_total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fwd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;caplen&lt;/span&gt;
                &lt;span class="n"&gt;traffic_total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;streamlen&lt;/span&gt;
                &lt;span class="n"&gt;url_list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myrequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullurl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;traffic_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;traffic_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tcp_snd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;traffic_send_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tcp_rcv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;traffic_receive_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url_list&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url_list&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;traffic_data&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pcap_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;target_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ucus.ucweb.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt; &lt;span class="nf"&gt;parsePcapFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcap_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# output: {'url_list': ['http://ucus.ucweb.com/usquery.php'], 'total': 10022, 'tcp_rcv': 6300, 'tcp_snd': 3722}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="Read More ..."&gt;Read More ...&lt;/h2&gt;
&lt;p&gt;博客地址：&lt;a href="http://debugtalk.com" rel="nofollow" target="_blank"&gt;http://debugtalk.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;微信公众号二维码：
&lt;img src="https://l.ruby-china.com/photo/2016/642460ab13367640b6ccdc96c8561a30.jpg" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Thu, 12 May 2016 21:07:00 +0800</pubDate>
      <link>https://ruby-china.org/topics/30003</link>
      <guid>https://ruby-china.org/topics/30003</guid>
    </item>
    <item>
      <title>Android App 持续集成性能测试：启动流量 (1)</title>
      <description>&lt;p&gt;本文对 Android App 的启动流量测试进行介绍。这里的启动流量指的是网络流量，即 App 在启动时发起网络请求和接收网络响应时传输的网络数据量。&lt;/p&gt;

&lt;p&gt;说起流量，也许大家的第一反应就是 tcpdump/wireshark 这类网络抓包工具。的确，Android 系统确实也支持&lt;code&gt;tcpdump&lt;/code&gt;工具，通过&lt;code&gt;tcpdump&lt;/code&gt;，我们可以实现非常精准的流量测试。但&lt;code&gt;tcpdump&lt;/code&gt;也有个问题，就是它捕捉到的流量是系统层面的，我们很难区分捕捉得到的流量数据是否都是当前 apk 产生的。&lt;/p&gt;

&lt;p&gt;其实，对于特定 apk 的整体流量数据，在 Android 系统中都会存储到对应文件中，我们完全可以通过读取对应文件来获得当前 apk 的流量信息。&lt;/p&gt;
&lt;h2 id="get app UID"&gt;get app UID&lt;/h2&gt;
&lt;p&gt;与流量相关的状态数据存储在&lt;code&gt;/proc/uid_stat/&amp;lt;UID&amp;gt;/&lt;/code&gt;目录下，其中，&lt;code&gt;&amp;lt;UID&amp;gt;&lt;/code&gt;表示 apk 对应的 UID。&lt;/p&gt;

&lt;p&gt;关于 UID，简单地进行下说明。在 Linux 系统中，UID 表示的是 User Identifier，主要用于表示是哪位用户运行了该程序。但在 Android 系统中，由于 Android 系统本身就为单用户系统，这时 UID 就被赋予了新的使命，主要用于实现数据共享。具体地，Android 系统为每个应用都分配了一个 UID，不同 apk 的 UID 几乎都是互不相同的，而对于不同 UID 的 apk，不能共享数据资源。之所以用“几乎”，是因为有时候同一厂家会存在多个产品，并且希望能在多个 apk 之间实现数据共享，这个时候，便可通过在 menifest 配置文件中指定相同的 sharedUserId，然后在 Android 系统中安装应用时便会分配相同的 UID。&lt;/p&gt;

&lt;p&gt;获取 app UID 的方式有多种，最简单的方式应该还是从&lt;code&gt;/data/system/packages.list&lt;/code&gt;中读取，并通过 apk 的&lt;code&gt;&amp;lt;PKGNAME&amp;gt;&lt;/code&gt;找到对应的 UID。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@hammerhead:/ &lt;span class="c"&gt;# cat /data/system/packages.list | grep com.UCMobile.trunk                   &lt;/span&gt;
com.UCMobile.trunk 10084 0 /data/data/com.UCMobile.trunk default 3003,1028,1015
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，10084 即是&lt;code&gt;com.UCMobile.trunk&lt;/code&gt;的 UID。&lt;/p&gt;
&lt;h2 id="获取流量数据"&gt;获取流量数据&lt;/h2&gt;
&lt;p&gt;流量数据分为接收流量（tcp_rcv）和发送流量（tcp_snd）两部分，这两个状态数值我们可以通过读取&lt;code&gt;/proc/uid_stat/&amp;lt;UID&amp;gt;&lt;/code&gt;目录下的两个文件得到。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shell@hammerhead:/ &lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/uid_stat/10084/tcp_rcv                          
3446837
shell@hammerhead:/ &lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/uid_stat/10084/tcp_snd                          
134366
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这种方式，我们就可以读取得到指定 apk 在当前时刻的累计流量数值。&lt;/p&gt;
&lt;h2 id="获得启动流量数据"&gt;获得启动流量数据&lt;/h2&gt;
&lt;p&gt;有了前面的基础，我们要测试启动流量就很好实现了。只需要在启动前采集下累计流量数值，然后启动应用，完成启动后再采集一次累计流量数值，前后两次累计数值的差值便是当次启动耗费的流量数。需要注意的是，由于很多时候 apk 在启动后，会在系统后台异步加载一些数据资源，因此为了保证我们采集到当次启动耗费的全部流量数值，我们在启动应用后最好能等待一段时间。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@hammerhead:/ &lt;span class="c"&gt;# cat /proc/uid_stat/10084/tcp_snd                           &lt;/span&gt;
15068
root@hammerhead:/ &lt;span class="c"&gt;# cat /proc/uid_stat/10084/tcp_rcv                           &lt;/span&gt;
98021

&lt;span class="c"&gt;# start app activity, sleep 10s&lt;/span&gt;

root@hammerhead:/ &lt;span class="c"&gt;# cat /proc/uid_stat/10142/tcp_snd                           &lt;/span&gt;
23268
root@hammerhead:/ &lt;span class="c"&gt;# cat /proc/uid_stat/10142/tcp_rcv                           &lt;/span&gt;
965651
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;采集到前后两次流量数值后，即可计算得到当次启动耗费的总流量。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;当次启动总流量 &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;23268 + 965651&lt;span class="o"&gt;)&lt;/span&gt; - &lt;span class="o"&gt;(&lt;/span&gt;15068 + 98021&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 875830 bytes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，这里的启动还分为好几种，包括首次安装启动、非首次安装启动、覆盖安装启动等。具体的启动方式可根据实际场景来定，但在统计流量的方法方面都是相同的。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;本文讲解了 Android App 启动流量测试的一种方法。然而，本次介绍的方法也存在一定局限性，因为&lt;code&gt;/proc/uid_stat/&amp;lt;UID&amp;gt;/&lt;/code&gt;目录下的&lt;code&gt;tcp_rcv&lt;/code&gt;和&lt;code&gt;tcp_snd&lt;/code&gt;文件中都只记录了总值，如果我们只关注总体的流量数值还好，但要是我们希望能测试得到更细化的数据，该方法就没法满足我们的测试需求了。&lt;/p&gt;

&lt;p&gt;举个例子，UC 浏览器国际版在启动后，会和美国的服务器进行通讯交互。现在，我们想测试 UC 浏览器国际版在启动后与美国服务器的通讯流量。&lt;/p&gt;

&lt;p&gt;显然，本文中介绍的方法是没法实现上述例子中的测试需求的。那例子中的场景要怎么测呢？这就还是得用到&lt;code&gt;tcpdump&lt;/code&gt;，在下一篇文章中我会再详细进行介绍。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="Read More ..."&gt;Read More ...&lt;/h2&gt;
&lt;p&gt;博客地址：&lt;a href="http://debugtalk.com" rel="nofollow" target="_blank"&gt;http://debugtalk.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;微信公众号二维码：
&lt;img src="https://l.ruby-china.com/photo/2016/642460ab13367640b6ccdc96c8561a30.jpg" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>debugtalk</author>
      <pubDate>Tue, 03 May 2016 19:36:10 +0800</pubDate>
      <link>https://ruby-china.org/topics/29905</link>
      <guid>https://ruby-china.org/topics/29905</guid>
    </item>
  </channel>
</rss>
