<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; Test</title>
	<atom:link href="https://blog.gmem.cc/category/work/test/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Mon, 06 Apr 2026 12:46:48 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>基于nGrinder进行负载测试</title>
		<link>https://blog.gmem.cc/load-testing-based-on-ngrinder</link>
		<comments>https://blog.gmem.cc/load-testing-based-on-ngrinder#comments</comments>
		<pubDate>Tue, 05 Mar 2019 08:34:34 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Test]]></category>
		<category><![CDATA[Groovy]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=25673</guid>
		<description><![CDATA[<p>简介 nGrinder是一个基于Grinder的压力测试平台。在此平台上你可以创建测试脚本、执行测试、监控目标服务器，同步的生成测试结果。 架构 基础组件 nGrinder由两种关键组件： 控制器：提供性能测试的Web接口，支持协调测试进程、收集并展示测试相关的统计信息，允许用户创建或修改测试脚本 代理，可以运行在两种模式下： Agent：运行线程、进程，将负载施加到目标服务器上 Monitor：监控目标服务器的性能，例如CPU/内存 代理启动后会尝试连接到控制器，然后被添加到AgentControllerServer —— 类似于Agent的池中。当用户发起一个性能测试后，nGrinder会创建一个协调Agents的SingleConsole（和Grinder中的Console作区分，起名叫SingleConsole），所需数量的Agent从AgentControllerServer交付给SingleConsole管理，SingleConsole向Agents发送测试脚本、测试资源，并控制测试流，直到测试完毕。测试完毕后，Agents回到AgentControllerServer的Agent池中，供后续测试重用。SingleConsole则回到ConsoleManager中。 nGrinder和Grinder最大的不同是，前者在控制器中持有多个Console实例、Agents。Console之间是相互独立的，并且可以并行的运行。Agent可以被预先分配给Console，也可以根据需要随时被请求、分配。nGrinder力求对Agent机器的最大化利用。其它测试平台，常常为测试任务预先分配Agents。用户为了争抢资源，常常会提前申请Agent而不充分利用。nGrinder仅在测试运行时才真正动态分配Agent。 架构图 集群架构 从3.1版本开始nGrinder开始支持控制器集群。性能测试可以被集群中的某个控制器调度，可以在多组Agent（称为Region）上执行。 &#160; 上图的架构中，包含两个控制器组成的集群： 所有控制器共享数据库、网络文件系统。所有控制器的[crayon-69d58fe350038773416693-i/]必须指向同一NFS路径 每个控制器可以有自己独特的属性、日志输出，这些信息存放在[crayon-69d58fe35003f521757856-i/] 所有控制器同步EhCache缓存中的共享数据 每个nGrinder控制器实例都可以独立的服务用户，需要注意的是nGrinder没有提供分布式会话，因此切换控制器后你需要重新登陆。 <a class="read-more" href="https://blog.gmem.cc/load-testing-based-on-ngrinder">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/load-testing-based-on-ngrinder">基于nGrinder进行负载测试</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>nGrinder是一个基于Grinder的压力测试平台。在此平台上你可以创建测试脚本、执行测试、监控目标服务器，同步的生成测试结果。</p>
<div class="blog_h2"><span class="graybg">架构</span></div>
<div class="blog_h3"><span class="graybg">基础组件</span></div>
<p>nGrinder由两种关键组件：</p>
<ol>
<li>控制器：提供性能测试的Web接口，支持协调测试进程、收集并展示测试相关的统计信息，允许用户创建或修改测试脚本</li>
<li>代理，可以运行在两种模式下：
<ol>
<li>Agent：运行线程、进程，将负载施加到目标服务器上</li>
<li>Monitor：监控目标服务器的性能，例如CPU/内存</li>
</ol>
</li>
</ol>
<p><span style="background-color: #c0c0c0;">代理</span>启动后会尝试连接到控制器，然后被<span style="background-color: #c0c0c0;">添加到AgentControllerServer</span> —— 类似于Agent的池中。当用户发起一个性能测试后，nGrinder会创建一个<span style="background-color: #c0c0c0;">协调Agents的SingleConsole</span>（和Grinder中的Console作区分，起名叫SingleConsole），所需数量的<span style="background-color: #c0c0c0;">Agent从AgentControllerServer交付给SingleConsole</span>管理，SingleConsole<span style="background-color: #c0c0c0;">向Agents发送测试脚本、测试资源，并控制测试流，直到测试完毕</span>。测试完毕后，<span style="background-color: #c0c0c0;">Agents回到AgentControllerServer的Agent池中</span>，供后续测试重用。<span style="background-color: #c0c0c0;">SingleConsole则回到ConsoleManager</span>中。</p>
<p>nGrinder和Grinder最大的不同是，前者在控制器中<span style="background-color: #c0c0c0;">持有多个Console实例、Agents</span>。Console之间是相互独立的，并且可以并行的运行。Agent可以被预先分配给Console，也可以根据需要随时被请求、分配。nGrinder力求对Agent机器的最大化利用。其它测试平台，常常为测试任务预先分配Agents。用户为了争抢资源，常常会提前申请Agent而不充分利用。nGrinder<span style="background-color: #c0c0c0;">仅在测试运行时才真正动态分配Agent</span>。</p>
<div class="blog_h3"><span class="graybg">架构图</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/03/ngrinder-architecture-0.png"><img class="aligncenter size-full wp-image-25701" src="https://blog.gmem.cc/wp-content/uploads/2019/03/ngrinder-architecture-0.png" alt="ngrinder-architecture-0" width="623" height="591" /></a></p>
<div class="blog_h2"><span class="graybg">集群架构</span></div>
<p>从3.1版本开始nGrinder开始支持控制器集群。性能测试可以被集群中的某个控制器调度，可以在多组Agent（称为Region）上执行。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/03/ngrinder-architecture-clustering.png"><img class="aligncenter size-full wp-image-25687" src="https://blog.gmem.cc/wp-content/uploads/2019/03/ngrinder-architecture-clustering.png" alt="ngrinder-architecture-clustering" width="654" height="353" /></a></p>
<p>&nbsp;</p>
<p>上图的架构中，包含两个控制器组成的集群：</p>
<ol>
<li>所有控制器共享数据库、网络文件系统。所有控制器的<span style="color: #24292e;"><pre class="crayon-plain-tag">${NGRINDER_HOME}</pre>必须指向同一NFS路径</span></li>
<li>每个控制器可以有自己独特的属性、日志输出，这些信息存放在<pre class="crayon-plain-tag">${NGRINDER_EX_HOME}</pre></li>
<li>所有控制器同步EhCache缓存中的共享数据</li>
</ol>
<p>每个nGrinder控制器实例都可以独立的服务用户，需要注意的是nGrinder没有提供分布式会话，因此切换控制器后你需要重新登陆。</p>
<div class="blog_h1"><span class="graybg">用户接口</span></div>
<div class="blog_h2"><span class="graybg">快速开始</span></div>
<p>在Quick Start中输入一个URL，可以自动生成一个测试脚本。测试脚本就是一段以Groovy语言编写的JUnit测试。</p>
<div class="blog_h2"><span class="graybg">测试配置</span></div>
<p>在Home页面点击Performance Test链接，可以进入已经定义的性能测试的列表。</p>
<p>点击列表项，可以进入单个性能测试的详情页面。其第一个选项卡是Test Configuration：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">配置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Agent</td>
<td>使用的代理数量</td>
</tr>
<tr>
<td>Vuser per agent</td>
<td>
<p>每个代理的“虚拟用户”的数量，可以展开设置线程、进程数量</p>
<p>总线程数 = 线程*进程 = 虚拟用户数 </p>
<p>和其它测试工具有所不同，nGrinder不去模拟真实的用户行为（比如说模拟10000个用户，在那每隔几秒发一个请求），而是专注于如何达到服务器的能力限制</p>
<p>如果你的确需要模拟真实用户行为，例如需要10000个虚拟用户， 你可以：</p>
<ol>
<li>准备两台性能较好的代理机器，例如2核心/6GB</li>
<li>编写Groovy脚本（比Jyhton更高效）</li>
<li>在system.conf配置 agent.max.vuser = 5000，解除默认3000的最大限制</li>
<li>调用<pre class="crayon-plain-tag">grinder.sleep(how_much_sleep_in_milliseconds)</pre>模拟用户思考导致的操作延迟</li>
<li>配置10进程 * 500线程</li>
</ol>
<p>如果代理机器的内存较低，例如4GB，则10个或更多进程会导致swap进而影响性能</p>
</td>
</tr>
<tr>
<td>Region</td>
<td>使用哪个区域中的Agent/Console</td>
</tr>
<tr>
<td>Target Host</td>
<td>
<p>被压测的主机，应当和脚本中所访问的主机一致。nGrinder控制器会在测试开始后，对Target host进行监控，你可以在图表中看到Target host的性能指标</p>
<p>你可以使用如下的形式来覆盖默认DNS解析：</p>
<pre class="crayon-plain-tag">www.gmem.cc:39.107.94.255 </pre>
</td>
</tr>
<tr>
<td>Duration</td>
<td>测试执行多久</td>
</tr>
<tr>
<td>Run Count</td>
<td>测试运行多少次</td>
</tr>
<tr>
<td>Enable Ramp-Up</td>
<td>在每个间隔之后增加进程数量</td>
</tr>
<tr>
<td>  Initial Count</td>
<td>启动测试时工作进程的数量</td>
</tr>
<tr>
<td>  Incremental Step</td>
<td>每个间隔增加进程的数量</td>
</tr>
<tr>
<td>  Initial Sleep Time</td>
<td>最初休眠的时间</td>
</tr>
<tr>
<td>  Interval</td>
<td>每个间隔的区间</td>
</tr>
<tr>
<td>Parameter</td>
<td>能够向测试传递单个名为param的参数，填写此参数的值</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">脚本管理</span></div>
<p>在Home页面点击Script链接，可以进入已经定义的脚本的列表。
<p>nGrinder内置了一个SVN服务器，用于管理用户编写的脚本文件以及资源，每个用户由独立的SVN存储库。你可以通过SVN客户端签出并管理脚本和资源，也可以通过nGrinder的UI来添加、删除、上传脚本或资源文件或目录。</p>
<div class="blog_h1"><span class="graybg">Groovy测试脚本</span></div>
<p>从3.2版本开始，nGrinder在Jython的基础上支持Groovy作为脚本编写语言。 </p>
<p>Groovy脚本是基于Junit的，因此对熟悉Java单元测试的人很友好。你可以通过SVN版本库管理压测脚本，甚至使用Maven来管理压测脚本的项目、在IDE中执行运行、调试压测脚本。</p>
<div class="blog_h2"><span class="graybg">脚本结构</span></div>
<div class="blog_h3"><span class="graybg">脚本模板</span></div>
<p>下面是一个基本的模板：</p>
<pre class="crayon-plain-tag">import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.plugin.http.HTTPRequest
import net.grinder.script.GTest
import net.grinder.script.Grinder
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess

import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith

import HTTPClient.HTTPResponse

// 测试用例必须附加如下注解
@RunWith(GrinderRunner)
class Test {

    public static GTest test;
    public static HTTPRequest request;

    // 每个进程被创建之前，执行的方法
    // 类似的还有@AfterProcess
    @BeforeProcess
    public static void beforeClass() {
        // 创建一个GTest实例
        test = new GTest(1, "${name}");
        // 录制对此HTTPRequest的任何调用，每次对它的调用都会增加TPS
        request = new HTTPRequest();
        test.record(request);
        grinder.logger.info("before process.");

        // 如果需要录制多个HTTPRequest，需要使用多个名字不同的GTest实例
    }

    // 每个线程被创建之前，执行的方法
    // 类似的还有@AfterThread 
    @BeforeThread
    public void beforeThread() {
        grinder.statistics.delayReports=true;
        grinder.logger.info("before thread.");
    }

    // 在停止测试之前，带有这个注解的方法会被持续不断的运行
    @Test
    public void test(){
        HTTPResponse result = request.GET("${url}");
        if (result.statusCode == 301 || result.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode);
        } else {
            // 可以进行断言，断言失败则绑定到当前线程的最后一次测试被标记为失败
            assertThat(result.statusCode, is(200));
        }
    }
}</pre>
<div class="blog_h3"><span class="graybg">特殊注解</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">注解</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>@BeforeProcess</td>
<td>
<p>Groovy脚本引擎创建新进程后执行的逻辑，例如：</p>
<ol>
<li>加载所有线程共享的静态资源</li>
<li>定义GTest并instrument测试目标</li>
</ol>
</td>
</tr>
<tr>
<td>@AfterProcess</td>
<td>
<p>Groovy脚本引擎在进程被终结后执行的逻辑，例如：</p>
<ol>
<li>关闭资源文件</li>
</ol>
</td>
</tr>
<tr>
<td>@BeforeThread</td>
<td>
<p>在进程的每个测试线程执行之前执行的逻辑，例如：</p>
<ol>
<li>登陆到目标系统</li>
<li>准备线程绑定变量</li>
</ol>
</td>
</tr>
<tr>
<td>@AfterThread</td>
<td>
<p>在进程的每个测试线程执行之后执行的逻辑，例如：</p>
<ol>
<li>登出系统</li>
</ol>
</td>
</tr>
<tr>
<td>@Before</td>
<td rowspan="2">每次测试之前、之后执行的逻辑</td>
</tr>
<tr>
<td>@After</td>
</tr>
<tr>
<td>@Test</td>
<td>
<p>需要执行的测试本身，会反复调用</p>
<p>你可以为多个方法添加此注解，但是需要注意，和JUnit不同，nGrinder会让所有这些<span style="background-color: #c0c0c0;">方法共享实例变量</span></p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">代码片断</span></div>
<div class="blog_h3"><span class="graybg">向脚本传递参数</span></div>
<p>在Test Configuration中指定的单个参数，可以这样获取：</p>
<pre class="crayon-plain-tag">// 名字固定为param
System.getProperty("param", "defaultValue")</pre>
<p>实际上，上述param参数是在命令行通过<pre class="crayon-plain-tag">-Dparam=value</pre>传递给测试进程的。 </p>
<p>引入Maven依赖ngrinder-groovy，可以调用net.grinder.util.GrinderUtils定义的静态方法，获取参数并进行类型转换。</p>
<div class="blog_h3"><span class="graybg">上下文信息</span></div>
<pre class="crayon-plain-tag">processnumber=grinder.processNumber                                 //获取当前的进程号，应该会相应返回0,1,2,3,4
totalprocess=grinder.getProperties().getInt("grinder.processes", 1) //获取测试的进程总数，返回5
threadnumber=grinder.threadNumber                                   //获取当前的线程号，应该会相应返回0,1
totalthread=grinder.getProperties().getInt("grinder.threads", 1)    //获取测试的线程总数,应该返回2
run=grinder.runNumber;                                              //获取当前的测试号，应该相应返回1,2,3
totalrun=grinder.getProperties().getInt("grinder.runs", 1);         //返回测试的总数，如3</pre>
<div class="blog_h3"><span class="graybg">HTTP插件参数</span></div>
<pre class="crayon-plain-tag">HTTPPluginControl.getConnectionDefaults().timeout = 6000 </pre>
<div class="blog_h3"><span class="graybg">POST请求</span></div>
<pre class="crayon-plain-tag">request = new HTTPRequest();
request.POST("http://www.google.com", [new NVPair("key1","value1"), new NVPair("key2":"value2")] as NVPair[])</pre>
<div class="blog_h3"><span class="graybg">响应断言</span></div>
<pre class="crayon-plain-tag">def result = request.GET("http://www.google.co.kr")
assertThat(result.statusCode, is(200)) // 状态码
assertThat(result.text, containsString("google")) // 响应体</pre>
<div class="blog_h3"><span class="graybg">录制测试方法</span></div>
<p>而不是录制HTTPRequest调用：</p>
<pre class="crayon-plain-tag">@RunWith(GrinderRunner)
class TestRunner {
    static GTest test1
    static GTest test2
    @BeforeProcess
    public static void beforeProcess() {
        test1 = new GTest(1,"test1_statistics")
        test2 = new GTest(2,"test2_statistics")
    }
 
    @BeforeThread
    public void beforeThread() {
        test1.record(this, "doTransaction1"); // 录制当前对象的doTransaction1调用到测试test1
        test2.record(this, "doTransaction2"); // 录制当前对象的doTransaction2调用到测试test2
        grinder.statistics.delayReports=true;
    }
 
    @Test
    public void doTransaction1(){
    }
 
    @Test
    public void doTransaction2(){
    }
}</pre>
<p>被录制方法的执行耗时会影响测试报告。</p>
<div class="blog_h3"><span class="graybg">设置权重</span></div>
<p>你可以设置每个测试方法，占用总计的测试次数的比例：</p>
<pre class="crayon-plain-tag">@RunRate(50)  // 此方法占所有测试次数（runs）的50%
@Test
public void test1() {
}

@RunRate(20)
@Test
public void test2() {
}</pre>
<p>如果总和不到100，则会有一定的测试次数（runs）什么都不干。</p>
<div class="blog_h3"><span class="graybg">调整日志级别</span></div>
<pre class="crayon-plain-tag">LoggerFactory.getLogger("worker").setLevel(Level.ERROR)</pre>
<div class="blog_h3"><span class="graybg">解析JSON</span></div>
<pre class="crayon-plain-tag">import groovy.json.JsonSlurper

def message = """
{
  "glossary": {
     "title": "example glossary",
      "GlossDiv": {
           "title": "S"
         }
      }
  }
}"""

def jsonMsg = new JsonSlurper().parseText(message)
grinder.logger.info(jsonMsg.glossary.title)</pre>
<div class="blog_h3"><span class="graybg">解析XML </span></div>
<pre class="crayon-plain-tag">def hello = new XmlParser().parseText(message);</pre>
<div class="blog_h3"><span class="graybg">流式处理大响应</span></div>
<pre class="crayon-plain-tag">byte[] buffer = new byte[1000];

HTTPResponse result = request.GET("http://www.google.com");

// 只读取1000字节就结束
def stream = result.getInputStream();
stream.read(buffer);
stream.close();</pre>
<div class="blog_h3"><span class="graybg">按测试线程决定行为</span></div>
<pre class="crayon-plain-tag">def threadId;
@BeforeThread
public void beforeThread() {
  threadId = GrinderUtils.threadUniqId; 
}

@Test
public doTest() {
   // 根据threadId进行条件分支
}</pre>
<div class="blog_h2"><span class="graybg">使用外部库</span></div>
<p>有两种方式：</p>
<ol>
<li>在测试脚本所在目录下，建立lib目录，其中存放jar</li>
<li>使用Maven依赖管理机制 </li>
</ol>
<div class="blog_h2"><span class="graybg">使用资源文件</span></div>
<ol>
<li>在测试脚本所在目录下，建立resources目录，其中存放资源文件，然后参考下面的脚本加载资源：<br />
<pre class="crayon-plain-tag">@BeforeProcess
public static void beforeProcess() {
    text = new File("./resources/resource1.txt").text;
} </pre>
</li>
<li>使用Maven，从类路径下加载：<br />
<pre class="crayon-plain-tag">ReflectionUtils.getCallingClass(0).getResourceAsStream("/resource1.txt");</pre>
</li>
</ol>
<div class="blog_h1"><span class="graybg">REST API</span></div>
<p>nGrinder 3.3提供了超过40个REST API，使用这些REST API你可以构建自己的nGrinder前端，或者让其它工具和nGrinder进行集成。REST API的功能包括：</p>
<ol>
<li>用户管理</li>
<li>代理管理</li>
<li>脚本管理</li>
<li>性能测试管理</li>
<li>系统管理</li>
</ol>
<p>REST API使用HTTP基本身份验证。</p>
<div class="blog_h2"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># 克隆现有性能测试，然后启动，226为测试ID
curl -u admin:admin http://ngrinder-host/perftest/api/226/clone_and_start
# 返回JSON形式、新创建的测试
# 你可以传递agentCount、scriptRevision参数来指定代理数量、脚本的修订版

# 查看现有的性能测试
curl -u admin:admin http://ngrinder-host/perftest/api/226

# 查看性能测试列表
curl -u admin:admin http://ngrinder-host/perftest/api</pre>
<div class="blog_h2"><span class="graybg">接口形式</span></div>
<p>对于nGrinder UI的URL（其URL Path称为topic），后缀以/api通常就是其对应的REST API端点：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 400px; text-align: center;">URL</td>
<td style="width: 75px; text-align: center;">动词</td>
<td style="text-align: center;">描述</td>
</tr>
</thead>
<tbody>
<tr>
<td>http://ngrinder-host/{topic}/api/</td>
<td>GET</td>
<td>获取元素列表</td>
</tr>
<tr>
<td>http://ngrinder-host/{topic}/api?ids={id,id}</td>
<td>GET</td>
<td>获取多个元素</td>
</tr>
<tr>
<td>http://ngrinder-host/{topic}/api/{kind}?ids={id,id}</td>
<td>GET</td>
<td>获得多个元素的某个特定信息</td>
</tr>
<tr>
<td>http://ngrinder-host/{topic}/api/{id}</td>
<td>GET</td>
<td>获得一个元素</td>
</tr>
<tr>
<td>http://ngrinder-host/{topic}/api/{id}/{kind}</td>
<td>GET</td>
<td>获取一个元素的某个特定信息</td>
</tr>
<tr>
<td>http://ngrinder-host/{topic}/api/</td>
<td>POST</td>
<td>新建元素</td>
</tr>
<tr>
<td>http://ngrinder-host/{topic}/api/{id}</td>
<td>PUT</td>
<td>修改一个元素</td>
</tr>
<tr>
<td>http://ngrinder-host/{topic}/api?ids={id,id}</td>
<td>PUT</td>
<td>修改多个元素</td>
</tr>
<tr>
<td>http://ngrinder-host/{topic}/api?action={action}&amp;ids={id,id}</td>
<td>PUT</td>
<td>指定若干元素上执行动作</td>
</tr>
<tr>
<td>http://ngrinder-host/{topic}/api/{id}?action={action}</td>
<td>PUT</td>
<td>在指定的元素上执行动作</td>
</tr>
<tr>
<td>http://ngrinder-host/{topic}/api/{id}</td>
<td>DELETE</td>
<td>删除一个元素</td>
</tr>
<tr>
<td>http://ngrinder-host/{topic}/api?ids={id,id}</td>
<td>DELETE</td>
<td>删除多个元素</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">接口列表</span></div>
<div class="blog_h3"><span class="graybg">PerfTest</span></div>
<p>参考：<a href="https://github.com/naver/ngrinder/wiki/REST-API-PerfTest">https://github.com/naver/ngrinder/wiki/REST-API-PerfTest</a></p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/load-testing-based-on-ngrinder">基于nGrinder进行负载测试</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/load-testing-based-on-ngrinder/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go语言单元测试和仿冒</title>
		<link>https://blog.gmem.cc/go-unitest-and-mock</link>
		<comments>https://blog.gmem.cc/go-unitest-and-mock#comments</comments>
		<pubDate>Wed, 08 Mar 2017 08:13:43 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[Test]]></category>
		<category><![CDATA[单元测试]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=25789</guid>
		<description><![CDATA[<p>testing 单元测试 Go语言提供了一个轻量级的测试框架，此框架由testing包和 [crayon-69d58fe35099d370784643-i/]命令组成。 要编写测试用例，你需要创建一个以 [crayon-69d58fe3509a1251843187-i/]结尾的源文件。该文件中包含一个或多个如下签名的函数： [crayon-69d58fe3509a3717343567/] 如果想让测试失败，调用testing.T的方法： [crayon-69d58fe3509a5425985734/] 基准测试 testing包还支持性能基准测试。要执行基准测试，调用命令[crayon-69d58fe3509a8680871021-i/]。 基准测试方法的签名如下： [crayon-69d58fe3509aa967766866/] 如果要基准测试并发执行的性能，可以使用[crayon-69d58fe3509ac711901216-i/]标记，并且调用助手函数：  [crayon-69d58fe3509ae445763421/] 验证样例  testing包还支持执行并验证样例代码。在被验证方法中，你可以通过注释声明期望的标准输出。 如果被验证方法的标准输出和注释匹配（不考虑首尾空白符），则验证通过。 示例： [crayon-69d58fe3509b0410435480/] 没有期望输出注释的Example方法，虽然编译，单是不会被执行。 样例方法的命名约定： <a class="read-more" href="https://blog.gmem.cc/go-unitest-and-mock">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-unitest-and-mock">Go语言单元测试和仿冒</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">testing</span></div>
<div class="blog_h2"><span class="graybg">单元测试</span></div>
<p>Go语言提供了一个轻量级的测试框架，此框架由testing包和 <pre class="crayon-plain-tag">go test -run</pre>命令组成。</p>
<p>要编写测试用例，你需要创建一个以 <pre class="crayon-plain-tag">_test.go</pre>结尾的源文件。该文件中包含一个或多个如下签名的函数：</p>
<pre class="crayon-plain-tag">// Test后面的第一个字母不能是小写
func TestXxx(t *testing.T)</pre>
<p>如果想让测试失败，调用testing.T的方法：</p>
<pre class="crayon-plain-tag">if err != nil {
    // 导致测试失败
    t.Errorf("Test failed: %s", err.Error())
    return

    // 或者直接panic
    t.Fatalf("Test failed: %s", err.Error())
}</pre>
<div class="blog_h2"><span class="graybg">基准测试</span></div>
<p>testing包还支持性能基准测试。要执行基准测试，调用命令<pre class="crayon-plain-tag">go test -bench</pre>。</p>
<p>基准测试方法的签名如下：</p>
<pre class="crayon-plain-tag">func BenchmarkXxx(*testing.B){
    // 目标逻辑必须运行b.N次，N根据实际情况调整，使测试结果尽量可靠
    for i := 0; i &lt; b.N; i++ {
        fmt.Sprintf("hello")
    }
}</pre>
<p>如果要基准测试并发执行的性能，可以使用<pre class="crayon-plain-tag">go test -cpu</pre>标记，并且调用助手函数： </p>
<pre class="crayon-plain-tag">func BenchmarkTemplateParallel(b *testing.B) {
    templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
    // 调用该助手函数触发并行测试
    b.RunParallel(func(pb *testing.PB) {
        var buf bytes.Buffer
        // 返回是否还有更多的迭代需要执行
        for pb.Next() {
            buf.Reset()
            templ.Execute(&amp;buf, "World")
        }
    })
}</pre>
<div class="blog_h2"><span class="graybg">验证样例 </span></div>
<p>testing包还支持执行并验证样例代码。在被验证方法中，你可以<span style="background-color: #c0c0c0;">通过注释声明期望的标准输出</span>。 如果被验证方法的标准输出和注释匹配（不考虑首尾空白符），则验证通过。</p>
<p>示例：</p>
<pre class="crayon-plain-tag">func ExampleHello() {
    fmt.Println("hello")
    // 期望输出必须以Output:开头
    // Output: hello
}

func ExampleSalutations() {
    fmt.Println("hello, and")
    fmt.Println("goodbye")
    // 可以放在多行注释中
    // Output:
    // hello, and
    // goodbye
}

func ExamplePerm() {
    for _, value := range Perm(4) {
        fmt.Println(value)
    }
    // 验证时不考虑输出行的顺序
    // Unordered output: 4
    // 2
    // 1
    // 3
    // 0
}</pre>
<p>没有期望输出注释的Example方法，虽然编译，单是不会被执行。</p>
<p>样例方法的命名约定：</p>
<pre class="crayon-plain-tag">func Example() { ... }
func ExampleF() { ... }     // 函数F的样例代码
func ExampleT() { ... }     // 类型T的样例代码
func ExampleT_M() { ... }   // 类型T的M方法的样例代码</pre>
<div class="blog_h2"><span class="graybg">跳过测试</span></div>
<p>在运行时，你可以跳过一部分单元测试、性能基准测试。</p>
<pre class="crayon-plain-tag">func TestTimeConsuming(t *testing.T) {
    // 在短测试模式下跳过此测试
    if testing.Short() {
        t.Skip("skipping test in short mode.")
    }
    ...
}</pre>
<div class="blog_h2"><span class="graybg">子测试</span></div>
<p>单元测试/基准测试支持“子测试”，你不需要定义额外的函数就可以实现表驱动（<span style="color: #222222;">table-driven）的基准测试或层次化的单元测试。</span>使用子测试还可以用来共享setup/teardown代码。</p>
<div class="blog_h3"><span class="graybg">串行执行</span></div>
<pre class="crayon-plain-tag">func TestFoo(t *testing.T) {
    // setup code here
    // t.Run运行t的子测试
    t.Run("A=1", func(t *testing.T) { ... })
    t.Run("A=2", func(t *testing.T) { ... })
    t.Run("B=1", func(t *testing.T) { ... })
    // teardown code here
}</pre>
<p>每个子测试必须具有唯一性的名称。传递给go test的名字是：顶级测试的名称/子测试的名称，以及一个可选的后缀序列号（去歧义）。</p>
<p>你可以通过命令行执行需要执行哪些测试、哪些子测试：</p>
<pre class="crayon-plain-tag">go test -run ''      # 运行所有测试
go test -run Foo     # 运行所有名称以Foo开头的顶级测试
go test -run Foo/A=  # 运行所有名称以Foo开头的顶级测试的匹配"A="的子测试（也就是子测试名称为A，不限制序列号）
go test -run /A=1    # 运行所有名称以Foo开头的顶级测试的匹配"A=1"的子测试</pre>
<div class="blog_h3"><span class="graybg">并行执行</span></div>
<p>你可以用如下的方法并行执行子测试：</p>
<pre class="crayon-plain-tag">// t是父测试
func TestGroupedParallel(t *testing.T) {
    for _, tc := range tests {
        tc := tc // 捕获变量供闭包使用
        // t.Run以子测试（of t）的形式运行tc
        t.Run(tc.Name, func(t *testing.T) {
            // 提示当前测试应当和其它并行（也就是同样调用t.Parallel的）测试并行的运行
            t.Parallel()
            ...
        })
    }
}</pre>
<p><span style="background-color: #c0c0c0;">所有子测试都完毕之后，父测试才会完成</span>。 </p>
<div class="blog_h2"><span class="graybg">testing.M</span></div>
<p>如果有以下需求：</p>
<ol>
<li>在测试之前之后执行setup/teardown逻辑</li>
<li>控制什么代码在主线程中执行</li>
</ol>
<p>你可以考虑使用：<pre class="crayon-plain-tag">func TestMain(m *testing.M)</pre> </p>
<p>如果测试源文件中包含如上签名的方法，那么go test不会直接运行测试，而是调用TestMain方法。</p>
<p>TestMain会在主线程中执行，你可以在调用m.Run运行具体测试之前、之后提供任何setup/teardown代码：</p>
<pre class="crayon-plain-tag">func TestMain(m *testing.M) {
    // setup
    // 如果传入参数，必须手工调用：
    flag.Parse()

    // test
    exitCode := m.Run()

    // teardown
    ...

    // exit
    os.Exit(exitCode)
}</pre>
<div class="blog_h1"><span class="graybg">ginkgo</span></div>
<p>一个行为驱动测试框架，参考：<a href="https://blog.gmem.cc/ginkgo-study-note">Ginkgo学习笔记</a></p>
<div class="blog_h1"><span class="graybg">gomock</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>gomock是一个通用的仿冒框架，可以和testing包很好的集成。</p>
<div class="blog_h2"><span class="graybg">mockgen</span></div>
<p>我们通过mockgen这个命令行工具来生成仿冒代码：</p>
<pre class="crayon-plain-tag">go install github.com/golang/mock/mockgen@v1.6.0</pre>
<div class="blog_h3"><span class="graybg">两种模式</span></div>
<p>mockgen提供两种不同的操作模式：</p>
<ol>
<li>source模式，该模式下，你可以从既有源文件来生成仿冒接口：<br />
<pre class="crayon-plain-tag">mockgen -source=foo.go</pre>
</li>
<li>反射模式，该模式下，会基于反射机制来自动识别需要仿冒的接口：<br />
<pre class="crayon-plain-tag">#       导入路径，.表示当前目录对应导入路径
#                           符号列表 
mockgen database/sql/driver Conn,Driver</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">命令行标记</span></div>
<p style="padding-left: 30px;">-source 包含需要被仿冒的接口的源文件<br />-destination 生成的仿冒源码存放到的目标文件<br />-package  生成的仿冒源码使用的包名<br />-imports 生成的仿冒源码需要明确使用的imports列表，格式foo=bar/baz,foo=bar/baz其中foo是导入包在仿冒源文件中的标识符，bar/baz是被导入的包的路径<br />-aux_files 辅助文件列表，辅助文件可以是主源文件中的内嵌接口的定义所在的文件，格式foo=bar/baz.go，foo是辅助文件所在包在仿冒源文件中的标识符<br />-build_flags 反射模式下传递给go build的标记<br />-mock_names 为每个生成的Mock指定名字，例如Repository=MockSensorRepository,Endpoint=MockSensorEndpoint，键是被仿冒接口的名字<br />-self_package 生成的代码的完整导入路径，用于防止循环导入<br />-copyright_file 版权头文件片段</p>
<div class="blog_h2"><span class="graybg">编写测试</span></div>
<p>被测试接口：</p>
<pre class="crayon-plain-tag">type Foo interface {
  Bar(x int) int
}

func SUT(f Foo) {
 // ...
}</pre>
<p>测试用例： </p>
<pre class="crayon-plain-tag">func TestFoo(t *testing.T) {
  ctrl := gomock.NewController(t)

  // Go 1.14+, mockgen 1.5.0+ 不再需要显式调用
  defer ctrl.Finish()

  m := NewMockFoo(ctrl)

  // 断言第一次（也是唯一一次）Bar调用的入参是99，其它任何调用都失败
  m.
    EXPECT().
    Bar(gomock.Eq(99)).
    Return(101)

  m.
    EXPECT(). // 期望每一次
    Bar(gomock.Eq(99)). // 入参99时
    DoAndReturn(func(_ int) int { // 休眠1秒然后会返回101
      time.Sleep(1*time.Second)
      return 101
    }).
    AnyTimes()


  SUT(m)
}</pre>
<div class="blog_h1"><span class="graybg">gock</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p><a href="https://github.com/h2non/gock">这是</a>一个HTTP流量仿冒和测试工具，特性包括：</p>
<ol>
<li>简单易用的链式调用API</li>
<li>声明式的仿冒DSL</li>
<li>内置助手用于简化XML/JSON响应仿冒</li>
<li>完整的基于正则式的HTTP请求匹配</li>
<li>基于请求方法、URL参数、头、体进行请求匹配</li>
<li>可扩展、可拔插的请求匹配规则</li>
<li>支持在仿冒/真实网络模式之间切换</li>
<li>可以和任何net/http兼容的客户端协作</li>
<li>网络延迟模拟</li>
<li>无外部依赖</li>
</ol>
<p>gock的工作原理：</p>
<ol>
<li>通过<span style="color: #24292e;">http.DefaultTransport或自定义的http.Transport来拦截HTTP出站请求</span></li>
<li>以FIFO声明顺序，将出站请求和HTTP仿冒期望（<span style="color: #24292e;">mock expectations）</span>池中的仿冒进行匹配</li>
<li>如果至少匹配一个仿冒，则此仿冒负责产生HTTP响应</li>
<li>如果没有匹配的仿冒，则默认报错，除非真实网络模式被开启 —— 导致执行真实的HTTP请求</li>
</ol>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">go get -u gopkg.in/h2non/gock.v1</pre>
<div class="blog_h2"><span class="graybg">样例</span></div>
<div class="blog_h3"><span class="graybg">简单例子</span></div>
<pre class="crayon-plain-tag">package test

import (
  "github.com/nbio/st"
  "gopkg.in/h2non/gock.v1"
  "io/ioutil"
  "net/http"
  "testing"
)

func TestSimple(t *testing.T) {
  // 清理
  defer gock.Off()

  // 仿冒对http://foo.com/bar的GET请求，返回200状态码和JSON响应
  gock.New("http://foo.com").
    Get("/bar").
    Reply(200).
    JSON(map[string]string{"foo": "bar"})

  // 下面的测试代码被拦截
  res, err := http.Get("http://foo.com/bar")
  st.Expect(t, err, nil)
  st.Expect(t, res.StatusCode, 200)

  body, _ := ioutil.ReadAll(res.Body)
  st.Expect(t, string(body)[:13], `{"foo":"bar"}`)

  // 期望没有未决的mock
  st.Expect(t, gock.IsDone(), true)
}</pre>
<div class="blog_h3"><span class="graybg">请求头匹配 </span></div>
<pre class="crayon-plain-tag">package test

import (
  "github.com/nbio/st"
  "gopkg.in/h2non/gock.v1"
  "io/ioutil"
  "net/http"
  "testing"
)

func TestMatchHeaders(t *testing.T) {
  defer gock.Off()

  gock.New("http://foo.com").
    // 匹配请求头，使用正则式
    MatchHeader("Authorization", "^foo bar$").
    MatchHeader("API", "1.[0-9]+").
    // 要求请求头Accept存在
    HeaderPresent("Accept").
    Reply(200).
    // 以字符串形式指定响应体
    BodyString("foo foo")


  req, err := http.NewRequest("GET", "http://foo.com", nil)
  req.Header.Set("Authorization", "foo bar")
  req.Header.Set("API", "1.0")
  req.Header.Set("Accept", "text/plain")
  ...
}</pre>
<div class="blog_h3"><span class="graybg">请求体匹配 </span></div>
<pre class="crayon-plain-tag">package test

import (
  "bytes"
  "github.com/nbio/st"
  "gopkg.in/h2non/gock.v1"
  "io/ioutil"
  "net/http"
  "testing"
)

func TestMockSimple(t *testing.T) {
  defer gock.Off()

  gock.New("http://foo.com").
    Post("/bar").
    // 以JSON方式匹配请求体
    MatchType("json").
    // 请求体必须包含foo字段，值为bar
    JSON(map[string]string{"foo": "bar"}).
    Reply(201).
    JSON(map[string]string{"bar": "foo"})

  body := bytes.NewBuffer([]byte(`{"foo":"bar"}`))
  res, err := http.Post("http://foo.com/bar", "application/json", body)
}</pre>
<div class="blog_h3"><span class="graybg">拦截客户端 </span></div>
<pre class="crayon-plain-tag">req, err := http.NewRequest("GET", "http://foo.com", nil)
client := &amp;http.Client{Transport: &amp;http.Transport{}}
gock.InterceptClient(client)</pre>
<div class="blog_h3"><span class="graybg">启用真实网络 </span></div>
<pre class="crayon-plain-tag">defer gock.DisableNetworking()

gock.EnableNetworking()
gock.New("http://httpbin.org")...</pre>
<div class="blog_h3"><span class="graybg">打印请求内容</span></div>
<pre class="crayon-plain-tag">gock.Observe(gock.DumpRequest)</pre>
<div class="blog_h2"><span class="graybg">实践 </span></div>
<div class="blog_h3"><span class="graybg">先声明仿冒再测试</span></div>
<p>编写实际测试逻辑之前，先把仿冒做好：</p>
<pre class="crayon-plain-tag">func TestFoo(t *testing.T) {
  defer gock.Off() // 再测试完成之后，刷空未决仿冒

  gock.New("http://server.com").
    Get("/bar").
    Reply(200).
    JSON(map[string]string{"foo": "bar"})

  // 在这里编写测试代码
}</pre>
<div class="blog_h3"><span class="graybg">竞态条件</span></div>
<p>如果你的测试代码是并发的，无比预先准备好仿冒。gock不是线程安全的。</p>
<div class="blog_h3"><span class="graybg">先具体再一般 </span></div>
<p>如果你需要编写一系列仿冒，那么，先编写具体化的、精确匹配请求的仿冒，然后再编写一般化的、通配的仿冒。</p>
<p>这样可以保证具体化的仿冒优先被测试是否匹配请求。</p>
<div class="blog_h3"><span class="graybg">仅拦截客户端一次</span></div>
<p>你仅仅需要在测试开始之前，拦截客户端一次：</p>
<pre class="crayon-plain-tag">gock.InterceptClient(client)</pre>
<div class="blog_h3"><span class="graybg">取消客户端拦截</span></div>
<p>在运行完测试场景之后，应当取消对客户端的拦截：</p>
<pre class="crayon-plain-tag">function TestGock (t *testing.T) {
	defer gock.Off()
	defer gock.RestoreClient(client)
}</pre>
<p>如果你使用的是http.DefaultClient或者http.DefaultTransport，不需要取消拦截。 </p>
<div class="blog_h1"><span class="graybg">govcr</span></div>
<p>手工编写Mock的困难在于如何精确的模拟依赖的行为。如果依赖已经开发完毕，而你需要实现可重复的、基于仿冒的单元测试，可以考虑将依赖的行为“录制”下来，并依此实现Mock。</p>
<p>govcr就是一个能实现HTTP交互录制/回放的开源项目，它同时支持回放成功、失败的HTTP事务。它<span style="background-color: #c0c0c0;">本质上是 http.Client的包装器</span>。</p>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">go get github.com/seborama/govcr

# 或者，明确指定兼容性版本，例如v4.x
go get gopkg.in/seborama/govcr.v4


# 导入路径
import "gopkg.in/seborama/govcr.v4"</pre>
<div class="blog_h2"><span class="graybg">术语</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>VCR</td>
<td>
<p>磁带录像机（Video Cassette Recorder），表示govcr提供的录制回放引擎，以及它产生的所有数据</p>
<p>VCR可以进行HTTP录制和回放，重复的请求回基于先前录制的信息  —— 位于磁盘中cassette文件中的track —— 直接返回，新请求则真正转发给真实服务器</p>
</td>
</tr>
<tr>
<td>cassette</td>
<td>一系列track的集合，默认保存在./govcr-fixtures目录下，形式为JSON文件，扩展名.cassette</td>
</tr>
<tr>
<td>Long Play cassette</td>
<td>以GZIP压缩的cassette，只需要以.gz后缀声明cassette名称即可启用</td>
</tr>
<tr>
<td>track</td>
<td>
<p>一个录制的HTTP请求，包括请求数据、响应数据，发生的错误</p>
<p>如果存在多个匹配请求的Track，则根据它们录制的顺序，依次进行回放</p>
</td>
</tr>
<tr>
<td>PCB</td>
<td>
<p>印刷电路板（Printed Circuit Board），能对VCR的某方面行为进行定制，例如：</p>
<ol>
<li>禁用录制</li>
<li>匹配track时，忽略某些请求头</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">VCRConfig</span></div>
<p>此结构用于配置govcr记录器：</p>
<pre class="crayon-plain-tag">vcr := govcr.NewVCR("MyCassette", &amp;govcr.VCRConfig{
    // 录像存储路径
    CassettePath: "./govcr-fixtures",
    // 禁用录制（但是如果有匹配的track仍然会回放）
    DisableRecording: true,
    // 禁用日志
    Logging: false,
    // 不录制TLS数据
    RemoveTLS: true,
})</pre>
<div class="blog_h2"><span class="graybg">过滤器</span></div>
<p>某些情况下，请求无法匹配到已经录制的track。例如请求中包含一个时间戳参数，后者动态变化的标识符。另外一些情况下，响应需要进行转换。应对这些场景，你需要使用过滤器：</p>
<ol>
<li>RequestFilter处理当前真实请求、Track上的请求，例如删除某个请求头，从而影响它们的匹配</li>
<li>ResponseFilter可以在返回给Client之前对响应进行预处理</li>
</ol>
<p>这些<span style="background-color: #c0c0c0;">转换操作不会持久化</span>，也就是它不会影响录制的Track。</p>
<div class="blog_h2"><span class="graybg">示例</span></div>
<div class="blog_h3"><span class="graybg">HelloWorld</span></div>
<pre class="crayon-plain-tag">package main

import (
    "fmt"
    "github.com/seborama/govcr"
)

const example1CassetteName = "MyCassette1"

func Example1() {
    vcr := govcr.NewVCR(example1CassetteName, nil)
    vcr.Client.Get("http://example.com/foo")
    fmt.Printf("%+v\n", vcr.Stats())
}</pre>
<div class="blog_h3"><span class="graybg">定制Transport</span></div>
<p>某些情况下，你的应用程序会创建自己的http.Client包装器，或者初始化自己的http.Transport（例如使用HTTPS的时候），你可以<span style="background-color: #c0c0c0;">传递自己的http.Client对象给VCR</span>，VCR会包装它，你需要使用包装后的http.Client：</p>
<pre class="crayon-plain-tag">package main

import (
    "crypto/tls"
    "fmt"
    "net/http"
    "time"

    "github.com/seborama/govcr"
)

const example2CassetteName = "MyCassette2"

type myApp struct {
    httpClient *http.Client
}

func (app myApp) Get(url string) {
    app.httpClient.Get(url)
}


func Example2() {
    // 创建自定义的Transport
    tr := http.DefaultTransport.(*http.Transport)
    tr.TLSClientConfig = &amp;tls.Config{
        InsecureSkipVerify: true, // 禁用TLS安全检查
    }

    myapp := &amp;myApp{
        // 使用自定义传输    
        httpClient: &amp;http.Client{
            Transport: tr,
            Timeout:   15 * time.Second,
        },
    }

    // 将Client传递给VCR
    vcr := govcr.NewVCR(example2CassetteName,
        &amp;govcr.VCRConfig{
            Client: myapp.httpClient,
    })

    // 使用注入后的HttpClient
    myapp.httpClient = vcr.Client

    myapp.Get("https://example.com/foo")
    fmt.Printf("%+v\n", vcr.Stats())
}</pre>
<div class="blog_h3"><span class="graybg">使用过滤器</span></div>
<p>下面是请求过滤器的例子：</p>
<pre class="crayon-plain-tag">package main

import (
    "fmt"
    "strings"
    "time"
    "net/http"
    "github.com/seborama/govcr"
)

const example4CassetteName = "MyCassette4"

func Example4() {
    vcr := govcr.NewVCR(example4CassetteName,
        &amp;govcr.VCRConfig{
            RequestFilters: govcr.RequestFilters{
                // 删除当前请求、Track请求的指定请求头后再进行匹配
                govcr.RequestDeleteHeaderKeys("X-Custom-My-Date"),
            },
            Logging: true,
        })

    req, err := http.NewRequest("POST", "http://example.com/foo", nil)
    if err != nil {
        fmt.Println(err)
    }
    req.Header.Add("X-Custom-My-Date", time.Now().String())
    vcr.Client.Do(req)
    fmt.Printf("%+v\n", vcr.Stats())
}</pre>
<p>下面的例子同时使用请求过滤器、响应过滤器：</p>
<pre class="crayon-plain-tag">func Example5() {
    vcr := govcr.NewVCR(example5CassetteName,
        &amp;govcr.VCRConfig{
            RequestFilters: govcr.RequestFilters{
                govcr.RequestDeleteHeaderKeys("X-Transaction-Id"),
            },
            ResponseFilters: govcr.ResponseFilters{
                 // 使用请求头中的X-Transaction-Id覆盖响应头中的X-Transaction-Id
                 govcr.ResponseTransferHeaderKeys("X-Transaction-Id"),
            },
            Logging: true,
        })
}</pre>
<p>下面展示过滤器的高级用法：</p>
<pre class="crayon-plain-tag">func Example6() {
	cfg := govcr.VCRConfig{
		Logging: true,
	}

	// 请求过滤器：将URL中的/order/{random} 重写为 /order/1234
	replacePath := govcr.RequestFilter(func(req govcr.Request) govcr.Request {
		// 重写
		req.URL.Path = "/order/1234"
		return req
	})
	// 条件性过滤器，仅当URL路径匹配example.com/order时才启用此过滤器
	replacePath = replacePath.OnPath(`example\.com\/order\/`)

	// 添加过滤器到VCRConfig
	cfg.RequestFilters.Add(replacePath)
	cfg.RequestFilters.Add(govcr.RequestDeleteHeaderKeys("X-Transaction-Id"))

	// 响应过滤器
	cfg.ResponseFilters.Add(
		// 覆盖响应头
		govcr.ResponseTransferHeaderKeys("X-Transaction-Id"),

		// 修改状态码
		func(resp govcr.Response) govcr.Response {
			if resp.StatusCode == http.StatusNotFound {
				resp.StatusCode = http.StatusAccepted
			}
			return resp
		},

		// 如果HTTP方法为GET，则添加响应头
		govcr.ResponseFilter(func(resp govcr.Response) govcr.Response {
			resp.Header.Add("method-was-get", "true")
			return resp
		}).OnMethod(http.MethodGet),

		// 如果HTTP方法为POST，则添加响应头
		govcr.ResponseFilter(func(resp govcr.Response) govcr.Response {
			resp.Header.Add("method-was-post", "true")
			return resp
		}).OnMethod(http.MethodPost),

		// 使用关联的请求的信息
		govcr.ResponseFilter(func(resp govcr.Response) govcr.Response {
			url := resp.Request().URL
			resp.Header.Add("get-url", url.String())
			return resp
		}).OnMethod(http.MethodGet),
	)
} </pre>
<div class="blog_h1"><span class="graybg">testify</span></div>
<p><a href="https://github.com/stretchr/testify">此包</a>提供通用的断言、仿冒功能。</p>
<div class="blog_h2"><span class="graybg">assert</span></div>
<p>该子包提供断言功能。示例：</p>
<pre class="crayon-plain-tag">package yours

import (
  "testing"
  "github.com/stretchr/testify/assert"
)

func TestAssertions(t *testing.T) {

  // 断言相等
  assert.Equal(t, 123, 123, "they should be equal")
  // 断言不等
  assert.NotEqual(t, 123, 456, "they should not be equal")

  // 断言为空
  assert.Nil(t, object)
  // 断言不为空
  if assert.NotNil(t, object) {
    // 进一步断言
    assert.Equal(t, "Something", object.Value)
  }

  // assertThat
  i := 0
  assert.Condition(t, func() bool { return i == 0 })

  // 断言函数调用有错误
  actualObj, err := SomeFunction()
  if assert.Error(t, err) {
    // ...
  }

  // 断言字符串、列表、映射包含指定的元素或子串
  assert.Contains(t, "Hello World", "World")
  assert.Contains(t, ["Hello", "World"], "World")
  assert.Contains(t, {"Hello": "World"}, "Hello")

  // 断言为空，也就是为nil、0、false，或者切片、通道长度为0
  assert.Empty(t, obj)
}</pre>
<div class="blog_h2"><span class="graybg">mock</span></div>
<p>该包提供仿冒功能：</p>
<pre class="crayon-plain-tag">package testify

import (
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"testing"
)

// 仿冒对象
type MyMockedObject struct {
	mock.Mock
}

// DoSomething是模拟的对象所具有的接口
func (m *MyMockedObject) DoSomething(number int) (bool, error) {

	// 告知仿冒对象的方法被调用的事实
	// 获取入参所关联的Arguments
	args := m.Called(number)
	// 返回Arguments的第1、2个元素
	return args.Bool(0), args.Error(1)

}

func TestSomething(t *testing.T) {

	// 实例化仿冒
	testObj := new(MyMockedObject)

	// 设置期望，如果入参123，则Argument的第1、2元素分别置为true nil
	testObj.On("DoSomething", 123).Return(true, nil)
	// 设置期望，对于任何入参，Argument的第1、2元素分别置为true nil
	testObj.On("DoSomething", mock.Anything).Return(true, nil)

	// 调用仿冒方法
	b, e := testObj.DoSomething(123)

	assert.Equal(t, b, true)
	assert.Empty(t, e)

	// 断言期望
	testObj.AssertExpectations(t)
}</pre>
<div class="blog_h2"><span class="graybg">suite</span></div>
<p>该包提供很多面向对象语言的单元测试工具（例如Junit）提供的功能。使用此包，你可以在结构中设计自己的测试套装（testing suite），编写setup/teardown方法： </p>
<pre class="crayon-plain-tag">package testify

import (
	"github.com/stretchr/testify/suite"
	"testing"
)

// 定义测试套装
type ExampleTestSuite struct {
	suite.Suite
}

// 套装运行前执行
func (suite *ExampleTestSuite) SetupSuite() {
}

// 每个测试运行前执行
func (suite *ExampleTestSuite) SetupTest() {
}

// 套件结构所有Test开头的方法，都是需要执行的测试
func (suite *ExampleTestSuite) TestExample() {
}

// 每个测试运行后执行
func (suite *ExampleTestSuite) TearDownTest() {
}

// 套装运行后执行
func (suite *ExampleTestSuite) TearDownSuite() {
}

// 为了支持go test，需要编写一个常规的Go测试方法
func TestExampleTestSuite(t *testing.T) {
	// 并在其方法体调用：
	suite.Run(t, new(ExampleTestSuite))
}</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-unitest-and-mock">Go语言单元测试和仿冒</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/go-unitest-and-mock/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>基于Spring Test和Mockito进行单元测试</title>
		<link>https://blog.gmem.cc/ut-with-spring-and-mockito</link>
		<comments>https://blog.gmem.cc/ut-with-spring-and-mockito#comments</comments>
		<pubDate>Tue, 08 Sep 2015 06:41:43 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[Test]]></category>
		<category><![CDATA[Mock]]></category>
		<category><![CDATA[SpringMVC]]></category>
		<category><![CDATA[单元测试]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=8180</guid>
		<description><![CDATA[<p>场景说明 本文引入一个简单的银行业务场景，用来阐述如何集成Spring Test、Junit、Mockito，以简化单元测试工作。该场景主要的业务代码如下： [crayon-69d58fe351362878929467/] 假设你已经实现了服务AccountService： [crayon-69d58fe351366083876455/] 而你的搭档负责的PersonService还没有开发完毕，如何方便的进行单元测试呢？ 在JUnit中集成Spring上下文的支持 你可能会觉得，我们不需要在单元测试中引入Spring。对于上面的例子的确可以这么说，它太简单了，AccountServiceImpl 依赖的PersonService完全可以通过setter手工注入。但是实际的开发场景要比这个例子复杂的多，待测试类可能和Spring管理的Beans存在很多关联，它可能依赖于Spring提供的数据源、事务管理器，等等。这些Bean如果都手工管理，将是相当繁琐无味的工作。 使用JUnit 4.x提供的注解[crayon-69d58fe35136a929620450-i/] ，可以指定单元测试的“运行类”，运行类必须继承自[crayon-69d58fe35136c621749893-i/] 并实现[crayon-69d58fe35136e169391174-i/] 方法。Spring Test框架提供的运行类是[crayon-69d58fe351370950614324-i/] ，使用该类可以轻松的将Spring和JUnit进行集成。该类的用法示例如下： [crayon-69d58fe351372291822140/] AccountService的测试用例 依据上一节的知识，我们编写集成Spring Test的测试用例： [crayon-69d58fe351375967925505/] 基于Mockito进行仿冒 引入Spring后，运行单元测试AccountServiceTest，会得到一个NoSuchBeanDefinitionException，这是因为AccountServiceImpl依赖的PersonService没有在Spring中注册。前面我们提到过，PersonService由搭档开发且尚未完成，这个时候要想单独测试AccountServiceImpl，那么就需要开发一个模拟的PersonService。最直接的模拟就是实现PersonService接口，但是不方便、工作量大，因此我们引入Mock框架：Mockito。 本文不去讨论Mockito的API细节，有兴趣的同学可以参考：使用Mockito进行单元测试 可以参考如下方式，单独将Mocketo和JUnit集成： <a class="read-more" href="https://blog.gmem.cc/ut-with-spring-and-mockito">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ut-with-spring-and-mockito">基于Spring Test和Mockito进行单元测试</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">场景说明</span></div>
<p>本文引入一个简单的银行业务场景，用来阐述如何集成Spring Test、Junit、Mockito，以简化单元测试工作。该场景主要的业务代码如下：</p>
<pre class="crayon-plain-tag">/**
 * 人员
 */
public class Person
{
    private int     id;
    private String  name;
    private Account defaultAccount;
}
/**
 * 账户
 */
public class Account
{
    private int  id;
    private int  balance;
    private Person person;
}
/**
 * 人员服务接口
 *
 */
public interface PersonService
{
    /**
     * 查询人员用户
     */
    Person getPerson( int id );
    /**
    * 得到人员默认账户
    */
    Account getDefaultAccount( Person p );
}
/**
 * 账户服务接口
 *
 */
public interface AccountService
{
    /**
     * 查询人员默认账户余额
     */
    int queryBalanceOfDefaultAccount( int personId );
}</pre>
<p>假设你已经实现了服务AccountService：</p>
<pre class="crayon-plain-tag">@Service ( "accountService" )
public class AccountServiceImpl implements AccountService
{
    private Map&lt;Integer, Object[]&gt; accountDatabase;

    @Inject
    private PersonService          personService;

    @PostConstruct
    public void init()
    {
        accountDatabase = new HashMap&lt;Integer, Object[]&gt;();
        //字段：账号,余额
        accountDatabase.put( 100, new Object[] { "6225100", 68861 } );
        accountDatabase.put( 101, new Object[] { "6225101", 1851 } );
        accountDatabase.put( 102, new Object[] { "6225102", 845 } );
        accountDatabase.put( 103, new Object[] { "6225103", 16598 } );
    }

    @Override
    public int queryBalanceOfDefaultAccount( int personId )
    {
        Person person = personService.getPerson( personId );
        Account defaultAccount = person.getDefaultAccount();
        return (Integer) accountDatabase.get( defaultAccount.getId() )[1];
    }

}</pre>
<p>而你的搭档负责的PersonService还没有开发完毕，如何方便的进行单元测试呢？</p>
<div class="blog_h2"><span class="graybg">在JUnit中集成Spring上下文的支持</span></div>
<p>你可能会觉得，我们不需要在单元测试中引入Spring。对于上面的例子的确可以这么说，它太简单了，AccountServiceImpl 依赖的PersonService完全可以通过setter手工注入。但是实际的开发场景要比这个例子复杂的多，待测试类可能和Spring管理的Beans存在很多关联，它可能依赖于Spring提供的数据源、事务管理器，等等。这些Bean如果都手工管理，将是相当繁琐无味的工作。</p>
<p>使用JUnit 4.x提供的注解<pre class="crayon-plain-tag">@RunWith</pre> ，可以指定单元测试的“运行类”，运行类必须继承自<pre class="crayon-plain-tag">org.junit.runner.Runner</pre> 并实现<pre class="crayon-plain-tag">run</pre> 方法。Spring Test框架提供的运行类是<pre class="crayon-plain-tag">SpringJUnit4ClassRunner</pre> ，使用该类可以轻松的将Spring和JUnit进行集成。该类的用法示例如下：</p>
<pre class="crayon-plain-tag">@RunWith ( SpringJUnit4ClassRunner.class ) //指定单元测试运行类
@ContextConfiguration ( locations = { "applicationContext.xml" } ) //指定Spring配置文件的位置
//很多情况下单元测试离不开事务，下面的注解指明使用的事务管理器
//如果defaultRollback为true，测试运行结束后，默认回滚事务，不影响数据库
@TransactionConfiguration ( transactionManager = "txManager", defaultRollback = true )
@Transactional //指定默认所有测试方法的事务特性
public class AccountServiceTest
{

    @Inject
    private SpringManagedBean bean; //任何Spring管理的Bean都可以注入到单元测试类

    @BeforeClass
    public static void setUpBeforeClass() throws Exception
    {
    }
    @AfterClass
    public static void tearDownAfterClass() throws Exception
    {
    }

    @Before
    public void setUp() throws Exception
    {
    }

    @After
    public void tearDown() throws Exception
    {
    }

    @Repeat ( 10 )//重复测试10次
    //该测试期望抛出IllegalArgumentException，测试超时1秒
    @Test ( expected = IllegalArgumentException.class, timeout = 1000 )
    @Rollback ( true )
    //测试完毕后回滚
    public void test()
    {
    }
}</pre>
<div class="blog_h3"><span class="graybg">AccountService的测试用例</span></div>
<p>依据上一节的知识，我们编写集成Spring Test的测试用例：</p>
<pre class="crayon-plain-tag">import static org.junit.Assert.*;
@RunWith ( SpringJUnit4ClassRunner.class )
@ContextConfiguration ( locations = { "/applicationContext.xml" } )
public class AccountServiceTest
{
    @Inject
    private AccountService accountService;

    @Test
    public void test()
    {
        assertEquals( 68861, accountService.queryBalanceOfDefaultAccount( 100 ) );
    }
}</pre>
<div class="blog_h2"><span class="graybg">基于Mockito进行仿冒</span></div>
<p>引入Spring后，运行单元测试AccountServiceTest，会得到一个NoSuchBeanDefinitionException，这是因为AccountServiceImpl依赖的PersonService没有在Spring中注册。前面我们提到过，PersonService由搭档开发且尚未完成，这个时候要想单独测试AccountServiceImpl，那么就需要开发一个<span style="background-color: #c0c0c0;">模拟</span>的PersonService。最直接的模拟就是实现PersonService接口，但是不方便、工作量大，因此我们引入<span style="background-color: #c0c0c0;">Mock框架：Mockito</span>。</p>
<p>本文不去讨论Mockito的API细节，有兴趣的同学可以参考：<a href="/ut-with-mockito">使用Mockito进行单元测试</a></p>
<p>可以参考如下方式，单独将Mocketo和JUnit集成：</p>
<pre class="crayon-plain-tag">@RunWith ( MockitoJUnitRunner.class ) //运行类
public class AccountServiceTest
{
    //AccountService所依赖的其它对象，会使用Mock注入，因此它引用的PersonService将是一个Mock
    @InjectMocks
    private AccountService accountService = new AccountServiceImpl();
    //自动生成一个PersonService的Mock实现
    @Mock
    private PersonService  personService;
}</pre>
<p>下面的代码则示例了如何把Spring也集成进来：</p>
<pre class="crayon-plain-tag">@RunWith ( SpringJUnit4ClassRunner.class )
//使用Spring提供的运行类
@ContextConfiguration ( locations = { "/applicationContext.xml" } )
public class AccountServiceTest
{
    @InjectMocks //该字段依赖的其它对象（PersonService），将使用仿冒注入
    @Inject //提示该字段本身由Spring自动注入
    private AccountService accountService;
    @Mock //由Mockito仿冒
    private PersonService  personService;
    @Before
    public void setUp()
    {
        //使得Mockito的注解生效
        MockitoAnnotations.initMocks( this );
    }
    @Test
    public void test()
    {
        //这里断点可以看到accountService.personService的类型是：
        //PersonService$$EnhancerByMockitoWithCGLIB$$61056d67
        //这是Mockito生成的仿冒类
        assertEquals( 68861, accountService.queryBalanceOfDefaultAccount( 100 ) );
    }
}</pre>
<p>注意：上面的集成<span style="background-color: #c0c0c0;">并没有解决</span>AccountServiceImpl对PersonService的依赖性，NoSuchBeanDefinitionException还会出现，除非使用Spring提供的“可选”依赖注入：</p>
<pre class="crayon-plain-tag">@Autowired ( required = false )
private PersonService          personService;</pre>
<p>但这种变通方式改变了应用语义，不应该使用。因此，到目前为止我们只能做到：在单元测试中用仿冒代替一个<span style="background-color: #c0c0c0;">既有</span>的Bean。</p>
<div class="blog_h2"><span class="graybg">粘合剂：Springockito</span></div>
<p>Springockito是针对Spring的一个小扩展，它可以简化Mockito仿冒的创建和管理，让Spring与之更无缝的集成：</p>
<pre class="crayon-plain-tag">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;beans  xmlns="http://www.springframework.org/schema/beans" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:context="http://www.springframework.org/schema/context" 
        xmlns:mockito="http://www.mockito.org/spring/mockito"
        
        xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
        http://www.mockito.org/spring/mockito https://cdn.gmem.cc/schema/spring-mockito.xsd
    "&gt;
    &lt;!-- 创建一个受Spring管理的PersonService仿冒，其它Bean很自然的可以获得注入 --&gt;
    &lt;mockito:mock id="personService" class="cc.gmem.study.sam.service.PersonService" /&gt;
    &lt;!-- 可以监控（Spying）一个Bean，但不影响它的任何行为，注意beanName必须与@Service指定的名称一致 --&gt;
    &lt;mockito:spy beanName="accountService" /&gt;
&lt;/beans&gt;</pre>
<p>现在可以把PersonService作为一个普通的Spring管理的Bean来看待，下面是最终的测试用例：</p>
<pre class="crayon-plain-tag">@Inject
private AccountService accountService;

@Inject
private PersonService  personService;  //注入mock，就像注入普通Bean一样

@Test
public void test()
{
    int id = 0;
    Person alex = new Person( id, "Alex", new Account( 100 ) );
    //对作为Bean的Mock进行打桩，设定后续方法调用的行为，就像为普通Mock打桩一样
    when( personService.getPerson( 0 ) ).thenReturn( alex );
    //验证结果
    assertEquals( 68861, accountService.queryBalanceOfDefaultAccount( id ) );

    //验证queryBalanceOfDefaultAccount方法被调用了一次
    verify( accountService ).queryBalanceOfDefaultAccount( id ); 
    //Mockito要求verify的入参必须是Mock，Springockito解除了这一限制
}</pre>
<p>我们使用了PersonService的Mock，而不要求它已经被实现、注册到Spring； 同时，我们可以对既有的Bean进行监控，而不要求它是一个Mock。</p>
<p> Springockito也提供了与XML配置等价的注解方式：</p>
<pre class="crayon-plain-tag">@RunWith ( SpringJUnit4ClassRunner.class )
//注意：必须修改loader为SpringockitoContextLoader.class
@ContextConfiguration ( loader = SpringockitoContextLoader.class, locations = { "/applicationContext.xml" } )
public class AccountServiceTest
{
    @WrapWithSpy  //mockito:spy
    @Inject
    private AccountService accountService;
    
    @ReplaceWithMock ( beanName = "personService" ) //mockito:mock
    @Inject
    private PersonService  personService;
}</pre>
<div class="blog_h2"><span class="graybg">Spring MVC的测试</span></div>
<p>Spring 3.2的Test子项目提供了类MockMvc，调用其<pre class="crayon-plain-tag">perform()</pre> 方法，可以触发一次“请求”，该调用会返回一个<pre class="crayon-plain-tag">ResultActions</pre> 接口。可以针对ResultActions执行一系列的<span style="background-color: #c0c0c0;">动作</span>和<span style="background-color: #c0c0c0;">断言</span>，或者返回<span style="background-color: #c0c0c0;">处理结果</span><pre class="crayon-plain-tag">MvcResult</pre> ，该接口提供以下方法：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 190px; text-align: center;"> 方法</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>andExpect(ResultMatcher)</td>
<td>执行期望（断言），一般会配合静态导入：MockMvcRequestBuilders.*、MockMvcResultMatchers.*使用。举例：<br />
<pre class="crayon-plain-tag">mockMvc.perform( get( "/person/1" ) )
//期望HTTP状态码为200
.andExpect( status.isOk() )
//期望响应的MIME类型为JSON
.andExpect( content().mimeType( MediaType.APPLICATION_JSON ) )
//期望响应中的JsonPath对应的值
//JsonPath类似于XPath，可参：http://goessner.net/articles/JsonPath/
.andExpect( jsonPath( "$.person.name" ).equalTo( "Alex" ) );

mockMvc.perform( post( "/form" ) )
//期望请求被重定向到URL
.andExpect( redirectedUrl( "/person/1" ) )
//期望模型属性的数量
.andExpect( model().size( 1 ) )
//期望模型属性的存在性
.andExpect( model().attributeExists( "person" ) );

//模拟请求参数、请求体
mockMvc.perform(
    post( "/form" )
        .param( "name", "Alex" )
        .param( "id", "0" )
        .param( "defacc", "100" )
        .content( "{}" ) //设置请求体，UTF-8字符串
);</pre>
</td>
</tr>
<tr>
<td>andDo(ResultHandler) </td>
<td>执行一个动作，一般会配合静态导入：MockMvcResultHandlers.*。举例：<br />
<pre class="crayon-plain-tag">mockMvc.perform( get( "/form" ) ).andDo( print() );</pre>
</td>
</tr>
<tr>
<td>MvcResult andReturn()</td>
<td>得到Mvc处理结果，从中可以HttpServletRequest、HttpServletResponse、MVC处理器、处理器抛出的异常、HandlerInterceptor、ModelAndView等内容</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">AccountController及其测试用例</span></div>
<p>我们开发一个简单的Controller类，它能够接收查询默认账户余额的请求，并返回一个包含人员、余额信息的映射：</p>
<pre class="crayon-plain-tag">@Controller ( "accountController" )
@RequestMapping ( "/account" )
public class AccountController
{
    @Inject
    private AccountService accountService;

    @RequestMapping ( "/{personId}/defacct/balance" )
    @ResponseBody
    public Map&lt;String, Integer&gt; queryBalanceOfDefaultAccount( @PathVariable int personId, @RequestParam long timestamp )
    {
        int balance = accountService.queryBalanceOfDefaultAccount( personId );
        Map&lt;String, Integer&gt; ret = new LinkedHashMap&lt;String, Integer&gt;();
        ret.put( "personId", personId );
        ret.put( "balance", balance );
        return ret;
    }
}</pre>
<p>假设我们的客户端需要JSON格式的数据，我们可以利用MockMvc来模拟客户端并验证。下面是一个简单的示例（包含JUnit测试类和Spring配置文件）：</p>
<pre class="crayon-plain-tag">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;beans  xmlns="http://www.springframework.org/schema/beans" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:context="http://www.springframework.org/schema/context" 
        xmlns:mockito="http://www.mockito.org/spring/mockito"
        
        xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
        http://www.mockito.org/spring/mockito https://cdn.gmem.cc/schema/spring-mockito.xsd
    "&gt;
    &lt;context:annotation-config /&gt;
    
    &lt;!-- 该接口尚未实现，必须仿冒 --&gt;
    &lt;mockito:mock id="personService" class="cc.gmem.study.sam.service.PersonService" /&gt;
    
    &lt;!-- 该接口虽已实现，但是为了隔离依赖单独测试MVC部分，我们这里使用仿冒 --&gt;
    &lt;mockito:mock id="accountService" class="cc.gmem.study.sam.service.AccountService" /&gt;
&lt;/beans&gt;</pre><br />
<pre class="crayon-plain-tag">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;beans xmlns="http://www.springframework.org/schema/beans" 
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mockito="http://www.mockito.org/spring/mockito"
       
       xsi:schemaLocation="
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
        http://www.mockito.org/spring/mockito https://cdn.gmem.cc/schema/spring-mockito.xsd
        
    "&gt;
    &lt;!-- 我们需要监控此Bean被调用的情况 --&gt;
    &lt;mockito:spy beanName="accountController" /&gt;
    &lt;context:component-scan base-package="cc.gmem.study.sam.ctrl" /&gt;

    &lt;mvc:annotation-driven /&gt;
    &lt;bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"&gt;
        &lt;property name="viewClass" value="org.springframework.web.servlet.view.JstlView" /&gt;
        &lt;property name="prefix" value="/WEB-INF/jsp/" /&gt;
        &lt;property name="suffix" value=".jsp" /&gt;
        &lt;property name="order" value="0" /&gt;
    &lt;/bean&gt;
&lt;/beans&gt;</pre><br />
<pre class="crayon-plain-tag">import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.http.MediaType.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith ( SpringJUnit4ClassRunner.class )
//提示该测试使用WebApplicationContext，需要3.2版本
//该注解的value用来说明不带Spring前缀的Resource（例如classpath:）的寻找路径
@WebAppConfiguration ( "src/main/webapp" )
//定义WebApplicationContext的层次，需要3.2.2版本
@ContextHierarchy ( {
        //这里目前不能使用load：SpringockitoContextLoader，可能Springockito注解尚不兼容3.2.2
        //因此我们使用XML方式配置Springockito
        //父上下文
        @ContextConfiguration ( locations = "classpath:applicationContext.xml" ),
        //子上下文
        @ContextConfiguration ( locations = "classpath:applicationContext-mvc.xml" ) 
} )
public class AccountControllerTest
{
    private MockMvc               mockMvc; //需要3.2版本
    
    @Inject
    private WebApplicationContext wac; //最低层次的上下文被注入
    
    @Inject
    private AccountController accountController; //被测试类
    
    @Inject
    private AccountService accountService;
    
    @Before
    public void setUp() throws Exception
    {
        //初始化MockMvc
        mockMvc = MockMvcBuilders.webAppContextSetup( wac ).build();
    }

    @Test
    public void test() throws Exception
    {
        int personId = 0;
        int balance = 1000;
        long timestamp = System.currentTimeMillis();
        
        /*仿冒打桩*/
        when(accountService.queryBalanceOfDefaultAccount( personId )).thenReturn( balance);
        
        /*HTTP请求模拟以及结果验证*/
        mockMvc.perform( 
                get( "/account/{personId}/defacct/balance", personId )
                .accept( APPLICATION_JSON ) //请求返回JSON格式的响应
                //设置请求头
                .header( "JSESSIONID", new Object[]{"aue60a2p2m8fe5s0t2m1am78t4"} )
                //设置请求参数
                .param( "timestamp", String.valueOf( timestamp ) )
        )
        .andExpect( status().isOk() )
        .andExpect( content().contentTypeCompatibleWith( APPLICATION_JSON ) ) //期望返回JSON格式的响应
        .andExpect( jsonPath( "$.balance" ).value( balance ) ) //JSONPath验证
        .andDo( print() ); //打印请求、响应和处理过程的详细信息，以便核查
        
        /*Mockito验证*/
        //验证accountController恰好被调用了一次
        verify( accountController ).queryBalanceOfDefaultAccount( personId, timestamp );
        //验证AccountService至少被调用了一次
        verify( accountService,atLeast( 1 ) ).queryBalanceOfDefaultAccount( personId );
        
        /*Junit验证*/
        assertTrue( true );
    }
}</pre>
<p>可以看到，我们在测试用例中基于MockMvc提供的丰富API，来构建仿冒的请求，并验证Spring MVC的响应。同时，我们使用Mockito来隔离AccountService服务，简化了依赖管理。 </p>
<div class="blog_h2"><span class="graybg">附录</span></div>
<p>本文使用的Maven POM依赖配置如下：</p>
<pre class="crayon-plain-tag">&lt;dependencies&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;javax.annotation&lt;/groupId&gt;
        &lt;artifactId&gt;jsr250-api&lt;/artifactId&gt;
        &lt;version&gt;1.0&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;javax.inject&lt;/groupId&gt;
        &lt;artifactId&gt;javax.inject&lt;/artifactId&gt;
        &lt;version&gt;1&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;javax.servlet&lt;/groupId&gt;
        &lt;artifactId&gt;servlet-api&lt;/artifactId&gt;
        &lt;version&gt;2.4&lt;/version&gt;
        &lt;scope&gt;provided&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;javax.servlet.jsp&lt;/groupId&gt;
        &lt;artifactId&gt;jsp-api&lt;/artifactId&gt;
        &lt;version&gt;2.0&lt;/version&gt;
        &lt;scope&gt;provided&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;javax.servlet&lt;/groupId&gt;
        &lt;artifactId&gt;jstl&lt;/artifactId&gt;
        &lt;version&gt;1.2&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;taglibs&lt;/groupId&gt;
        &lt;artifactId&gt;standard&lt;/artifactId&gt;
        &lt;version&gt;1.1.2&lt;/version&gt;
        &lt;scope&gt;provided&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;javax.el&lt;/groupId&gt;
        &lt;artifactId&gt;el-api&lt;/artifactId&gt;
        &lt;version&gt;2.2&lt;/version&gt;
        &lt;scope&gt;provided&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-core&lt;/artifactId&gt;
        &lt;version&gt;3.2.3.RELEASE&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-jdbc&lt;/artifactId&gt;
        &lt;version&gt;3.2.3.RELEASE&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-beans&lt;/artifactId&gt;
        &lt;version&gt;3.2.3.RELEASE&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-orm&lt;/artifactId&gt;
        &lt;version&gt;3.2.3.RELEASE&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;
        &lt;version&gt;3.2.3.RELEASE&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;com.fasterxml.jackson.core&lt;/groupId&gt;
        &lt;artifactId&gt;jackson-core&lt;/artifactId&gt;
        &lt;version&gt;2.0.4&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;com.fasterxml.jackson.core&lt;/groupId&gt;
        &lt;artifactId&gt;jackson-databind&lt;/artifactId&gt;
        &lt;version&gt;2.0.4&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;com.fasterxml.jackson.core&lt;/groupId&gt;
        &lt;artifactId&gt;jackson-annotations&lt;/artifactId&gt;
        &lt;version&gt;2.0.4&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;log4j&lt;/groupId&gt;
        &lt;artifactId&gt;log4j&lt;/artifactId&gt;
        &lt;version&gt;1.2.15&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.slf4j&lt;/groupId&gt;
        &lt;artifactId&gt;slf4j-log4j12&lt;/artifactId&gt;
        &lt;version&gt;1.6.1&lt;/version&gt;
    &lt;/dependency&gt;


    &lt;dependency&gt;
        &lt;groupId&gt;junit&lt;/groupId&gt;
        &lt;artifactId&gt;junit&lt;/artifactId&gt;
        &lt;version&gt;4.11&lt;/version&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-test&lt;/artifactId&gt;
        &lt;version&gt;3.2.3.RELEASE&lt;/version&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.mockito&lt;/groupId&gt;
        &lt;artifactId&gt;mockito-all&lt;/artifactId&gt;
        &lt;version&gt;1.9.0&lt;/version&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.kubek2k&lt;/groupId&gt;
        &lt;artifactId&gt;springockito&lt;/artifactId&gt;
        &lt;version&gt;1.0.9&lt;/version&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.kubek2k&lt;/groupId&gt;
        &lt;artifactId&gt;springockito-annotations&lt;/artifactId&gt;
        &lt;version&gt;1.0.9&lt;/version&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;com.jayway.jsonpath&lt;/groupId&gt;
        &lt;artifactId&gt;json-path-assert&lt;/artifactId&gt;
        &lt;version&gt;0.9.1&lt;/version&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ut-with-spring-and-mockito">基于Spring Test和Mockito进行单元测试</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ut-with-spring-and-mockito/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>使用Mockito进行单元测试</title>
		<link>https://blog.gmem.cc/ut-with-mockito</link>
		<comments>https://blog.gmem.cc/ut-with-mockito#comments</comments>
		<pubDate>Sat, 11 Feb 2012 08:23:00 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[Test]]></category>
		<category><![CDATA[Mock]]></category>
		<category><![CDATA[单元测试]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=8196</guid>
		<description><![CDATA[<p>Mockito简介 Mockito是一个Java仿冒框架，所谓仿冒，就是创建一个“虚假”的类，来模拟一个类的行为。之所以需要“仿冒”，可能出于下列考虑： 某个类已经进入单元测试，而协作类尚未开发完成。此时需要快速模拟出协作类，避免耽误测试进度 协作类的实现可能存在缺陷，这是可以使用仿冒隔离其影响，仿冒严格的按照接口规约返回处理结果 模拟难以在单元测试阶段获得实例的对象，这些对象往往和环境、容器相关，例如HttpServletRequest Mockito关注三件事件：Mock的创建、验证（verification）和打桩（stubbing）。所谓验证，就是断言预期的行为（方法调用）发生；所谓打桩，就是设置Mock在特定条件（入参）下的行为（返回值、异常）。 功能说明 简单的例子 下面的小例子说明何为验证、打桩： [crayon-69d58fe351b41464988988/] 可以看到，都Mock的每一种调用，我们都可以监控到；相应的Mock可以迅速、准确的模拟出真实对象的行为， 而忽略任何行为的实现细节。 未打桩前，Mock这样处理返回值： 返回Object的方法，Mock默认返回null 返回集合的方法，Mock默认返回空集合 返回数字的方法，Mock默认返回0 返回布尔的方法，Mock默认返回false 对Mock的打桩可以进行多次，后面的会覆盖前面的打桩。 参数匹配 对Mock进行打桩或者验证时时，可以有多种匹配入参的方式： [crayon-69d58fe351b45439235435/] 参数捕获 <a class="read-more" href="https://blog.gmem.cc/ut-with-mockito">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ut-with-mockito">使用Mockito进行单元测试</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">Mockito简介</span></div>
<p>Mockito是一个Java仿冒框架，所谓仿冒，就是创建一个“虚假”的类，来模拟一个类的行为。之所以需要“仿冒”，可能出于下列考虑：</p>
<ol>
<li>某个类已经进入单元测试，而协作类<span style="background-color: #c0c0c0;">尚未开发完成</span>。此时需要<span style="background-color: #c0c0c0;">快速模拟</span>出协作类，避免耽误测试进度</li>
<li>协作类的实现可能存在缺陷，这是可以使用仿冒<span style="background-color: #c0c0c0;">隔离其影响</span>，仿冒严格的按照接口规约返回处理结果</li>
<li>模拟<span style="background-color: #c0c0c0;">难以在单元测试阶段获得</span>实例的对象，这些对象往往和环境、容器相关，例如HttpServletRequest</li>
</ol>
<p>Mockito关注三件事件：Mock的创建、验证（verification）和打桩（stubbing）。所谓验证，就是断言预期的行为（方法调用）发生；所谓打桩，就是设置Mock在特定条件（入参）下的行为（返回值、异常）。</p>
<div class="blog_h2"><span class="graybg">功能说明</span></div>
<div class="blog_h3"><span class="graybg">简单的例子</span></div>
<p>下面的小例子说明何为验证、打桩：</p>
<pre class="crayon-plain-tag">import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

//制作一个仿冒
Map&lt;String, Object&gt; map = mock( Map.class );

//对仿冒进行调用
map.put( "Key", 1024 );
map.clear();

//验证：仿冒的某个方法以指定的参数被调用一次
verify( map ).put( "Key", new Integer( 1024 ) ); //只进行equals匹配

//验证：仿冒的某个方法至少被调用一次
verify( map, atLeast( 1 ) ).clear();

//打桩：尚未打桩的入参调用，返回null
assertEquals( null, map.get( "Any" ) );
//打桩：当以Hello为键获取值时，返回World
when( map.get( "Hello" ) ).thenReturn( "World" );
assertEquals( "World", map.get( "Hello" ) );</pre>
<p>可以看到，都Mock的每一种调用，我们都可以监控到；相应的Mock可以迅速、准确的模拟出真实对象的行为， 而忽略任何行为的实现细节。</p>
<p>未打桩前，Mock这样处理返回值：</p>
<ol>
<li>返回Object的方法，Mock默认返回null</li>
<li>返回集合的方法，Mock默认返回空集合</li>
<li>返回数字的方法，Mock默认返回0</li>
<li>返回布尔的方法，Mock默认返回false</li>
</ol>
<p>对Mock的打桩可以进行多次，后面的会覆盖前面的打桩。</p>
<div class="blog_h3"><span class="graybg">参数匹配</span></div>
<p>对Mock进行打桩或者验证时时，可以有多种匹配入参的方式：</p>
<pre class="crayon-plain-tag">List list = mock( List.class );
when( list.get( anyInt() ) ).thenReturn( null );//任何整数参数
when( list.addAll( anyInt(), anyCollection() ) ).thenReturn( null );//两个任意参数
//实现参数匹配器接口，进行任意复杂度的匹配
when( list.get( argThat( new ArgumentMatcher&lt;Integer&gt;() {
    @Override
    public boolean matches( Object item )
    {
        Integer index = (Integer) item;
        return index &lt; 100;
    }
} ) ) ).thenReturn( null );</pre>
<div class="blog_h3"><span class="graybg">参数捕获</span></div>
<pre class="crayon-plain-tag">List list = mock( List.class );
ArgumentCaptor&lt;Integer&gt; argument = ArgumentCaptor.forClass( Integer.class );
list.add( 1 );//这个参数会被捕获
verify( list ).get( argument.capture() ); //执行捕获
//后续对参数进行进一步验证
assertEquals( new Integer( 1 ), argument.getValue() );</pre>
<div class="blog_h3"><span class="graybg">返回值设定</span></div>
<pre class="crayon-plain-tag">List list = mock( List.class );
//返回固定值
when( list.get( 0 ) ).thenReturn( 10 );
//设置每次调用返回不同值
when( list.get( 0 ) ).thenReturn( 1, 2, 3 );
//抛出异常，不返回
when( list.get( 0 ) ).thenThrow( new RuntimeException() );
//依据入参决定返回值
when( list.get( anyInt() ) ).then( new Answer&lt;Object&gt;() {

    public Object answer( InvocationOnMock invocation ) throws Throwable
    {
        return "El-" + invocation.getArguments()[0];
    }
} );
//在部分仿冒时，调用真实方法
when( list.get( 0 ) ).thenCallRealMethod();

//连续仿冒
when( list.get( 0 ) )
        .thenReturn( 1 ) //第一次调用的返回值
        .thenReturn( 2 )//第二次调用的返回值
        .thenThrow( new RuntimeException() ); //以后调用的返回值

//设置未打桩方法的默认返回值
mock( List.class, new Answer&lt;Object&gt;() {

    @Override
    public Object answer( InvocationOnMock invocation ) throws Throwable
    {
        return null;
    }
} );</pre>
<div class="blog_h3"><span class="graybg">void方法打桩</span></div>
<pre class="crayon-plain-tag">//什么都不作
doNothing().when( list ).clear();
//抛出异常
doThrow( new RuntimeException() ).when( list ).clear();</pre>
<div class="blog_h3"><span class="graybg">调用次数验证</span></div>
<pre class="crayon-plain-tag">List list = mock( List.class );
//验证基于某个精确实参的调用发生的次数
verify( list, times( 1 ) ).add( 1 ); //一次
verify( list, times( 2 ) ).add( 2 ); //二次
verify( list, atLeastOnce() ).add( 1 ); //至少一次
verify( list, atLeast( 3 ) ).add( 3 ); //至少三次
verify( list, atMost( 3 ) ).add( 3 ); //至多三次
//参数匹配也适用于验证的时候：
verify( list, atMost( 3 ) ).add( argThat( new ArgumentMatcher&lt;Object&gt;() {
    public boolean matches( Object argument )
    {
        return false;
    }
} ) );

//断言Mock上没有发生任何未验证的调用
verifyNoMoreInteractions( list );</pre>
<div class="blog_h3"><span class="graybg">监控真实对象</span></div>
<pre class="crayon-plain-tag">List&lt;Integer&gt; list = new ArrayList&lt;Integer&gt;();
List&lt;Integer&gt; spy = spy( list );

//对真实对象进行打桩
when( spy.size() ).thenReturn( 100 );

//另外一种打桩方式，防止异常
doReturn( 1 ).when( spy ).get( 0 );
//下面这种方式是不行的，因为未打桩前会调用真实方法，导致IndexOutOfBoundsException 
when( spy.get( 0 ) ).thenReturn( 1 );


spy.add( 1 );//调用真实的方法
spy.size();//调用仿冒的方法
verify( spy ).add( 1 ); //验证调用了一次</pre>
<div class="blog_h3"><span class="graybg">部分仿冒</span></div>
<p>有两种方式进行部分仿冒：Spy一个对象，或者Mock一个类（非接口）：</p>
<pre class="crayon-plain-tag">List list = spy(new LinkedList());
List list = mock(LinkedList.class);
//调用真实对象的方法
when(list.clear()).thenCallRealMethod();</pre>
<div class="blog_h3"><span class="graybg">注解的支持</span></div>
<pre class="crayon-plain-tag">//如果要启用注解支持，可以使用该运行类
//或者手工调用MockitoAnnotations.initMocks(this)
@RunWith ( MockitoJUnitRunner.class )
public class MockitoTest
{
    //自动创建Mock
    @Mock
    private List&lt;Object&gt; list;

}</pre>
<div class="blog_h3"><span class="graybg">其它</span></div>
<pre class="crayon-plain-tag">List mock = mock(List.class);
when(mock.size()).thenReturn(10);

reset(mock);  //重置仿冒

//单行调用即完成的Mock
Car boringStubbedCar = when(mock(Car.class).shiftGear()).thenThrow(EngineNotStarted.class).getMock();</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ut-with-mockito">使用Mockito进行单元测试</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ut-with-mockito/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
