<?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; 单元测试</title>
	<atom:link href="https://blog.gmem.cc/tag/%e5%8d%95%e5%85%83%e6%b5%8b%e8%af%95/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Fri, 17 Apr 2026 09:20:32 +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>Ginkgo学习笔记</title>
		<link>https://blog.gmem.cc/ginkgo-study-note</link>
		<comments>https://blog.gmem.cc/ginkgo-study-note#comments</comments>
		<pubDate>Tue, 19 Nov 2019 07:06:04 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[单元测试]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=29913</guid>
		<description><![CDATA[<p>简介 Ginkgo /ˈɡɪŋkoʊ / 是Go语言的一个行为驱动开发（BDD， Behavior-Driven Development）风格的测试框架，通常和库Gomega一起使用。Ginkgo在一系列的“Specs”中描述期望的程序行为。 Ginkgo集成了Go语言的测试机制，你可以通过[crayon-69e29c2da6fdd948891999-i/]来运行Ginkgo测试套件。 Ginkgo 安装 [crayon-69e29c2da6fe3377536477/] 起步 创建套件 假设我们想给books包编写Ginkgo测试，则首先需要使用命令创建一个Ginkgo test suite： [crayon-69e29c2da6fe5235631911/] 上述命令会生成文件： [crayon-69e29c2da6fe7864759278/] 现在，使用命令[crayon-69e29c2da6fea014926732-i/]或者[crayon-69e29c2da6fec767080296-i/]即可执行测试套件。 添加Spec 上面的空测试套件没有什么价值，我们需要在此套接下编写测试（Spec）。虽然可以在books_suite_test.go中编写测试，但是推荐分离到独立的文件中，特别是包中有多个需要被测试的源文件的情况下。 <a class="read-more" href="https://blog.gmem.cc/ginkgo-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ginkgo-study-note">Ginkgo学习笔记</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>Ginkgo /ˈɡɪŋkoʊ / 是Go语言的一个行为驱动开发（BDD， Behavior-Driven Development）风格的测试框架，通常和库Gomega一起使用。Ginkgo在一系列的“Specs”中描述期望的程序行为。</p>
<p>Ginkgo集成了Go语言的测试机制，你可以通过<pre class="crayon-plain-tag">go test</pre>来运行Ginkgo测试套件。</p>
<div class="blog_h1"><span class="graybg">Ginkgo</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">go get -u github.com/onsi/ginkgo/ginkgo</pre>
<div class="blog_h2"><span class="graybg">起步</span></div>
<div class="blog_h3"><span class="graybg">创建套件</span></div>
<p>假设我们想给books包编写Ginkgo测试，则首先需要使用命令创建一个Ginkgo test suite：</p>
<pre class="crayon-plain-tag">cd pkg/books
ginkgo bootstrap</pre>
<p>上述命令会生成文件：</p>
<pre class="crayon-plain-tag">package books_test

import (
    // 使用点号导入，把这两个包导入到当前命名空间
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
    "testing"
)

func TestBooks(t *testing.T) {
    // 将Ginkgo的Fail函数传递给Gomega，Fail函数用于标记测试失败，这是Ginkgo和Gomega唯一的交互点
    // 如果Gomega断言失败，就会调用Fail进行处理
    RegisterFailHandler(Fail)

    // 启动测试套件
    RunSpecs(t, "Books Suite")
}</pre>
<p>现在，使用命令<pre class="crayon-plain-tag">ginkgo</pre>或者<pre class="crayon-plain-tag">go test</pre>即可执行测试套件。</p>
<div class="blog_h3"><span class="graybg">添加Spec</span></div>
<p>上面的空测试套件没有什么价值，我们需要在此套接下编写测试（Spec）。虽然可以在books_suite_test.go中编写测试，但是推荐<span style="background-color: #c0c0c0;">分离到独立的文件</span>中，特别是包中有多个需要被测试的源文件的情况下。</p>
<p>执行命令<pre class="crayon-plain-tag">ginkgo generate book</pre>可以为源文件book.go生成测试：</p>
<pre class="crayon-plain-tag">package books_test

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	// 为了方便，被测试包被导入当前命名空间
	. "ginkgo-study/pkg/books"
)

// 顶级的Describe容器

// Describe块用于组织Specs，其中可以包含任意数量的：
//    BeforeEach：在Spec（It块）运行之前执行，嵌套Describe时最外层BeforeEach先执行
//    AfterEach：在Spec运行之后执行，嵌套Describe时最内层AfterEach先执行
//    JustBeforeEach：在It块，所有BeforeEach之后执行
//    Measurement

// 可以在Describe块内嵌套Describe、Context、When块
var _ = Describe("Book", func() {

})</pre>
<p>我们可以添加一些Specs：</p>
<pre class="crayon-plain-tag">// 使用Describe、Context容器来组织Spec
var _ = Describe("Book", func() {
	var (
		// 通过闭包在BeforeEach和It之间共享数据
		longBook  Book
		shortBook Book
	)
	// 此函数用于初始化Spec的状态，在It块之前运行。如果存在嵌套Describe，则最
	// 外面的BeforeEach最先运行
	BeforeEach(func() {
		longBook = Book{
			Title:  "Les Miserables",
			Author: "Victor Hugo",
			Pages:  1488,
		}

		shortBook = Book{
			Title:  "Fox In Socks",
			Author: "Dr. Seuss",
			Pages:  24,
		}
	})

	Describe("Categorizing book length", func() {
		Context("With more than 300 pages", func() {
			// 通过It来创建一个Spec
			It("should be a novel", func() {
				// Gomega的Expect用于断言
				Expect(longBook.CategoryByLength()).To(Equal("NOVEL"))
			})
		})

		Context("With fewer than 300 pages", func() {
			It("should be a short story", func() {
				Expect(shortBook.CategoryByLength()).To(Equal("SHORT STORY"))
			})
		})
	})
})</pre>
<div class="blog_h3"><span class="graybg">断言失败</span></div>
<p>除了调用Gomega之外，你还可以调用Fail函数直接断言失败：</p>
<pre class="crayon-plain-tag">Fail("Failure reason")</pre>
<p>Fail会记录当前进行的测试，并且触发panic，当前Spec的后续断言不会再进行。</p>
<p>通常情况下Ginkgo会<span style="background-color: #c0c0c0;">从panic中恢复，并继续下一个测试</span>。但是，如果你启动了一个Goroutine，并在其中触发了断言失败，则不会自动恢复，必须手工调用GinkgoRecover：</p>
<pre class="crayon-plain-tag">It("panics in a goroutine", func(done Done) {
    go func() {
        // 如果doSomething返回false则下面的defer会确保从panic中恢复
        defer GinkgoRecover()
        // Ω和Expect功能相同
        Ω(doSomething()).Should(BeTrue())

        // 在Goroutine中需要关闭done通道
        close(done)
    }()
})</pre>
<div class="blog_h3"><span class="graybg">记录日志</span></div>
<p>全局的GinkgoWriter可以用于写日志。默认情况下GinkgoWriter仅仅在测试失败时将日志Dump到标准输出，以冗长模式（<pre class="crayon-plain-tag">ginkgo -v</pre> 或 <pre class="crayon-plain-tag">go test -ginkgo.v</pre>）运行Ginkgo时则会立即输出。</p>
<p>如果通过Ctrl + C中断测试，则Ginkgo会立即输出写入到GinkgoWriter的内容。联用<pre class="crayon-plain-tag">--progress</pre>则Ginkgo会在BeforeEach/It/AfterEach之前输出通知到GinkgoWriter，这个特性便于诊断卡住的测试。</p>
<div class="blog_h3"><span class="graybg">传递参数</span></div>
<p>直接使用flag包即可：</p>
<pre class="crayon-plain-tag">var myFlag string
func init() {
    flag.StringVar(&amp;myFlag, "myFlag", "defaultvalue", "myFlag is used to control my behavior")
}</pre>
<p>执行测试时使用<pre class="crayon-plain-tag">ginkgo -- --myFlag=xxx</pre>传递参数。</p>
<div class="blog_h2"><span class="graybg">测试的结构</span></div>
<div class="blog_h3"><span class="graybg">It</span></div>
<p>你可以在Describe、Context这两种容器块内编写Spec，每个Spec写在It块中。</p>
<p>为了贴合自然语言，可以使用It的别名Specify：</p>
<pre class="crayon-plain-tag">Describe("The foobar service", func() {
  Context("when calling Foo()", func() {
    Context("when no ID is provided", func() {
      // 应该返回ErrNoID错误
      Specify("an ErrNoID error is returned", func() {
      })
    })
  })
})</pre>
<div class="blog_h3"><span class="graybg">BeforeEach</span></div>
<p>多个Spec共享的、测试准备逻辑，可以放到BeforeEach块中。</p>
<p>在BeforeEach、AfterEach块中进行断言是允许的。</p>
<p>存在容器嵌套时，最外层BeforeEach先运行。</p>
<div class="blog_h3"><span class="graybg">AfterEach</span></div>
<p>多个Spec共享的、测试清理逻辑，可以放到AfterEach块中。存在容器嵌套时，最内层AfterEach先运行。</p>
<div class="blog_h3"><span class="graybg">Describe/Context</span></div>
<p>两者的区别：</p>
<ol>
<li>Describe用于描述你的代码的一个行为</li>
<li>Context用于区分上述行为的不同情况，通常为参数不同导致</li>
</ol>
<p>下面是一个例子：</p>
<pre class="crayon-plain-tag">// 这是关于Book服务测试
var _ = Describe("Book", func() {
    var (
        book Book
        err error
    )

    BeforeEach(func() {
        book, err = NewBookFromJSON(`{
            "title":"Les Miserables",
            "author":"Victor Hugo",
            "pages":1488
        }`)
    })
    // 测试加载Book行为
    Describe("loading from JSON", func() {
        // 如果正常解析JSON
        Context("when the JSON parses succesfully", func() {
            It("should populate the fields correctly", func() {
                // 期望                相等
                Expect(book.Title).To(Equal("Les Miserables"))
                Expect(book.Author).To(Equal("Victor Hugo"))
                Expect(book.Pages).To(Equal(1488))
            })

            It("should not error", func() {
                // 期望      没有发生错误
                Expect(err).NotTo(HaveOccurred())
            })
        })
        // 如果无法解析JSON
        Context("when the JSON fails to parse", func() {
            BeforeEach(func() {
                // 这是一个BDD反模式，可以用JustBeforeEach
                book, err = NewBookFromJSON(`{
                    "title":"Les Miserables",
                    "author":"Victor Hugo",
                    "pages":1488oops
                }`)
            })

            It("should return the zero-value for the book", func() {
                // 期望          为零
                Expect(book).To(BeZero())
            })

            It("should error", func() {
                // 期望        发生了错误
                Expect(err).To(HaveOccurred())
            })
        })
    })

    Describe("Extracting the author's last name", func() {
        It("should correctly identify and return the last name", func() {
            Expect(book.AuthorLastName()).To(Equal("Hugo"))
        })
    })
})</pre>
<div class="blog_h3"><span class="graybg">JustBeforeEach</span></div>
<p>上面的例子中，内层Spec需要尝试从无效JSON创建Book，因此它调用NewBookFromJSON对book变量进行覆盖。这种做法是推荐的，应该使用JustBeforeEach，这种块在任何BeforeEach执行完毕后执行：</p>
<pre class="crayon-plain-tag">var _ = Describe("Book", func() {
    var (
        book Book
        err error
        json string
    )
    // 准备默认JSON
    BeforeEach(func() {
        json = `{
            "title":"Les Miserables",
            "author":"Victor Hugo",
            "pages":1488
        }`
    })

    JustBeforeEach(func() {
        // 按需，根据默认数据/无效JSON创建book，避免NewBookFromJSON的重复调用（如果代价很高的话……）
        book, err = NewBookFromJSON(json)
    })

    Describe("loading from JSON", func() {
        Context("when the JSON parses succesfully", func() {
        })

        Context("when the JSON fails to parse", func() {
            BeforeEach(func() {
                // 覆盖默认JSON为无效JSON
                json = `{
                    "title":"Les Miserables",
                    "author":"Victor Hugo",
                    "pages":1488oops
                }`
            })
        })
    })
})</pre>
<p>在上面的例子中，JustBeforeEach<span style="background-color: #c0c0c0;">解耦了创建（Creation）和配置（Configuration）</span>这两个阶段。</p>
<div class="blog_h3"><span class="graybg">JustAfterEach</span></div>
<p>紧跟着It之后运行，在所有AfterEach执行之前。</p>
<div class="blog_h3"><span class="graybg">BeforeSuite/AfterSuite</span></div>
<p>在整个测试套件执行之前/之后，进行准备/清理。和套件代码写在一起：</p>
<pre class="crayon-plain-tag">func TestBooks(t *testing.T) {
    RegisterFailHandler(Fail)

    RunSpecs(t, "Books Suite")
}

var _ = BeforeSuite(func() {
    dbClient = db.NewClient()
    err = dbClient.Connect(dbRunner.Address())
    Expect(err).NotTo(HaveOccurred())
})

var _ = AfterSuite(func() {
    dbClient.Cleanup()
})</pre>
<p>这两个块都支持异步执行，只需要给函数传递一个Done参数即可。 </p>
<div class="blog_h3"><span class="graybg">By</span></div>
<p>此块用于给逻辑复杂的块添加文档：</p>
<pre class="crayon-plain-tag">var _ = Describe("Browsing the library", func() {
    BeforeEach(func() {
        By("Fetching a token and logging in")
    })

    It("should be a pleasant experience", func() {
        By("Entering an aisle")
    })
})</pre>
<p>传递给By的字符串会发送给GinkgoWriter，如果测试失败你可以看到。</p>
<p>你可以传递一个可选的函数给By，此函数会立即执行。</p>
<div class="blog_h2"><span class="graybg">性能测试</span></div>
<p>使用Measure块可以进行性能测试，所有It能够出现的地方，都可以使用Measure。和It一样，Measure会生成一个新的Spec。</p>
<p>传递给Measure的闭包函数必须具有Benchmarker入参：</p>
<pre class="crayon-plain-tag">Measure("it should do something hard efficiently", func(b Benchmarker) {
    // 执行一段逻辑并即时
    runtime := b.Time("runtime", func() {
        output := SomethingHard()
        Expect(output).To(Equal(17))
    })

    // 断言 执行时间             小于 0.2 秒
    Ω(runtime.Seconds()).Should(BeNumerically("&lt;", 0.2), "SomethingHard() shouldn't take too long.")

    // 录制任意数据
    b.RecordValue("disk usage (in MB)", HowMuchDiskSpaceDidYouUse())
}, 10)</pre>
<p>执行时间、你录制的任意数据的最小、最大、平均值均会在测试完毕后打印出来。</p>
<div class="blog_h2"><span class="graybg">CLI</span></div>
<div class="blog_h3"><span class="graybg">运行测试</span></div>
<pre class="crayon-plain-tag"># 运行当前目录中的测试
ginkgo
# 运行其它目录中的测试
ginkgo /path/to/package /path/to/other/package ...

# 递归运行所有子目录中的测试
ginkgo -r ...</pre>
<div class="blog_h3"><span class="graybg">传递参数</span></div>
<p>传递参数给测试套件：  </p>
<pre class="crayon-plain-tag">ginkgo -- PASS-THROUGHS-ARGS</pre>
<div class="blog_h3"><span class="graybg">跳过某些包</span></div>
<pre class="crayon-plain-tag"># 跳过某些包
ginkgo -skipPackage=PACKAGES,TO,SKIP</pre>
<div class="blog_h3"><span class="graybg">超时控制</span></div>
<p>选项<pre class="crayon-plain-tag">-timeout</pre>用于控制套件的最大运行时间，如果超过此时间仍然没有完成，认为测试失败。默认24小时。</p>
<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>--reportPassed</td>
<td>打印通过的测试的详细信息</td>
</tr>
<tr>
<td>--v</td>
<td>冗长模式</td>
</tr>
<tr>
<td>--trace</td>
<td>打印所有错误的调用栈</td>
</tr>
<tr>
<td>--progress</td>
<td>打印进度信息</td>
</tr>
</tbody>
</table>
<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>-race</td>
<td>启用竞态条件检测</td>
</tr>
<tr>
<td>-cover</td>
<td>启用覆盖率测试</td>
</tr>
<tr>
<td>-tags</td>
<td>指定编译器标记</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Spec Runner</span></div>
<div class="blog_h3"><span class="graybg">Pending Spec</span></div>
<p>你可以标记一个Spec或容器为Pending，这样默认<span style="background-color: #c0c0c0;">情况下不会运行</span>它们。定义块时使用P或X前缀：</p>
<pre class="crayon-plain-tag">PDescribe("some behavior", func() { ... })
PContext("some scenario", func() { ... })
PIt("some assertion")
PMeasure("some measurement")

XDescribe("some behavior", func() { ... })
XContext("some scenario", func() { ... })
XIt("some assertion")
XMeasure("some measurement")</pre>
<p>默认情况下Ginkgo会为每个Pending的Spec打印描述信息，使用命令行选项<pre class="crayon-plain-tag">--noisyPendings=false</pre>禁止该行为。 </p>
<div class="blog_h3"><span class="graybg">Skiping Spec</span></div>
<p>P或X前缀会在编译期将Spec标记为Pending，你也可以在运行期跳过特定的Spec：</p>
<pre class="crayon-plain-tag">It("should do something, if it can", func() {
    if !someCondition {
        // 跳过此Spec，不需要Return语句
        Skip("special condition wasn't met")
    }
})</pre>
<div class="blog_h3"><span class="graybg">Focused Specs</span></div>
<p>一个很常见的需求是，可以选择运行Spec的一个子集。Ginkgo提供两种机制满足此需求：</p>
<ol>
<li> 将容器或Spec标记为Focused，这样默认情况下Ginkgo仅仅运行Focused Spec：<br />
<pre class="crayon-plain-tag">FDescribe("some behavior", func() { ... })
 FContext("some scenario", func() { ... })
 FIt("some assertion", func() { ... })</pre>
</li>
</ol>
<ol>
<li>
<p>在命令行中传递正则式：<pre class="crayon-plain-tag">--focus=REGEXP</pre> 或/和 <pre class="crayon-plain-tag">--skip=REGEXP</pre>，则Ginkgo仅仅运行/跳过匹配的Spec</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Parallel Specs</span></div>
<p>Ginkgo支持并行的运行Spec，它实现方式是，创建go test子进程并在其中运行共享队列中的Spec。</p>
<p>使用<pre class="crayon-plain-tag">ginkgo -p</pre>可以启用并行测试，Ginkgo会自动创建适当数量的节点（进程）。你也可以指定节点数量：<pre class="crayon-plain-tag">ginkgo -nodes=N</pre>。</p>
<p>如果你的测试代码需要和外部进程交互，或者创建外部进程，在并行测试上下文中需要谨慎的处理。最简单的方式是在BeforeSuite方法中为每个节点创建外部资源。</p>
<p>如果所有Spec需要共享一个外部进程，则可以利用SynchronizedBeforeSuite、SynchronizedAfterSuite：</p>
<pre class="crayon-plain-tag">var _ = SynchronizedBeforeSuite(func() []byte {
    // 在第一个节点中执行
    port := 4000 + config.GinkgoConfig.ParallelNode

    dbRunner = db.NewRunner()
    err := dbRunner.Start(port)
    Expect(err).NotTo(HaveOccurred())

    return []byte(dbRunner.Address())
}, func(data []byte) {
    // 在所有节点中执行
    dbAddress := string(data)

    dbClient = db.NewClient()
    err = dbClient.Connect(dbAddress)
    Expect(err).NotTo(HaveOccurred())
})</pre>
<p>上面的例子，为所有节点创建共享的数据库，然后为每个节点创建独占的客户端。 SynchronizedAfterSuite的回调顺序则正好相反：</p>
<pre class="crayon-plain-tag">var _ = SynchronizedAfterSuite(func() {
    // 所有节点
    dbClient.Cleanup()
}, func() {
    // 第一个节点
    dbRunner.Stop()
}) </pre>
<div class="blog_h1"><span class="graybg">Gomega</span></div>
<p>这时Ginkgo推荐使用的断言（Matcher）库。</p>
<div class="blog_h2"><span class="graybg">联用</span></div>
<div class="blog_h3"><span class="graybg">和Ginkgo</span></div>
<p>注册Fail处理器即可：</p>
<pre class="crayon-plain-tag">gomega.RegisterFailHandler(ginkgo.Fail)</pre>
<div class="blog_h3"><span class="graybg">和Go测试框架</span></div>
<pre class="crayon-plain-tag">func TestFarmHasCow(t *testing.T) {
    // 创建Gomega对象
    g := NewGomegaWithT(t)

    f := farm.New([]string{"Cow", "Horse"})
    // 进行断言
    g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow")
}</pre>
<div class="blog_h2"><span class="graybg">断言</span></div>
<div class="blog_h3"><span class="graybg">Ω/Expect</span></div>
<p>两种断言语法本质是一样的，只是命名风格有些不同：</p>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(Equal(EXPECTED))
Expect(ACTUAL).To(Equal(EXPECTED))

Ω(ACTUAL).ShouldNot(Equal(EXPECTED))
Expect(ACTUAL).NotTo(Equal(EXPECTED))
Expect(ACTUAL).ToNot(Equal(EXPECTED))</pre>
<div class="blog_h3"><span class="graybg">错误处理</span></div>
<p>对于返回多个值的函数：</p>
<pre class="crayon-plain-tag">func DoSomethingHard() (string, error) {}

result, err := DoSomethingHard()
// 断言没有发生错误
Ω(err).ShouldNot(HaveOccurred())
Ω(result).Should(Equal("foo"))</pre>
<p>对于仅仅返回一个error的函数： </p>
<pre class="crayon-plain-tag">func DoSomethingHard() (string, error) {}

Ω(DoSomethingSimple()).Should(Succeed())</pre>
<div class="blog_h3"><span class="graybg">断言注解</span></div>
<p>进行断言时，可以提供格式化字符串，这样断言失败可以方便的知道原因：</p>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(Equal(EXPECTED), "My annotation %d", foo)

Expect(ACTUAL).To(Equal(EXPECTED), "My annotation %d", foo)

Expect(ACTUAL).To(Equal(EXPECTED), func() string { return "My annotation" })</pre>
<div class="blog_h3"><span class="graybg">简化输出</span></div>
<p>断言失败时，Gomega打印牵涉到断言的对象的递归信息，输出可能很冗长。</p>
<p>format包提供了一些全局变量，调整这些变量可以简化输出。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">变量 = 默认值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>format.MaxDepth = 10</td>
<td>打印对象嵌套属性的最大深度</td>
</tr>
<tr>
<td>format.UseStringerRepresentation = false</td>
<td>
<p>默认情况下，Gomega不会调用Stringer.String()或GoStringer.GoString()方法来打印对象的字符串表示</p>
<p>字符串表示通常人类可读但是信息量较小</p>
<p>设置为true则打印字符串表示，可以简化输出</p>
</td>
</tr>
<tr>
<td>format.PrintContextObjects = false</td>
<td>默认情况下，Gomega不会打印context.Context接口的内容，因为通常非常冗长</td>
</tr>
<tr>
<td>format.TruncatedDiff = true</td>
<td>截断长字符串，仅仅打印差异</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">异步断言</span></div>
<p>Gomega提供了两个函数，用于异步断言。</p>
<p>传递给Eventually、Consistently的函数，如果返回多个值，则第一个返回值用于匹配，<span style="background-color: #c0c0c0;">其它值断言为nil或零值</span>。</p>
<div class="blog_h3"><span class="graybg">Eventually</span></div>
<p>阻塞并轮询参数，直到能通过断言：</p>
<pre class="crayon-plain-tag">// 参数是闭包，调用函数
Eventually(func() []int {
    return thing.SliceImMonitoring
}).Should(HaveLen(2))

// 参数是通道，读取通道
Eventually(channel).Should(BeClosed())
Eventually(channel).Should(Receive())

// 参数也可以是普通变量，读取变量
Eventually(myInstance.FetchNameFromNetwork).Should(Equal("archibald"))

// 可以和gexec包的Session配合
Eventually(session).Should(gexec.Exit(0)) // 命令最终应当以0退出
Eventually(session.Out).Should(Say("Splines reticulated")) // 检查标准输出</pre>
<p>可以指定超时、轮询间隔：</p>
<pre class="crayon-plain-tag">Eventually(func() []int {
    return thing.SliceImMonitoring
}, TIMEOUT, POLLING_INTERVAL).Should(HaveLen(2))</pre>
<div class="blog_h3"><span class="graybg">Consistently</span></div>
<p>检查断言是否在一定时间段内总是通过：</p>
<pre class="crayon-plain-tag">Consistently(func() []int {
    return thing.MemoryUsage()
}, DURATION, POLLING_INTERVAL).Should(BeNumerically("&lt;", 10))</pre>
<p>Consistently也可以用来断言最终不会发生的事件，例如下面的例子：</p>
<pre class="crayon-plain-tag">Consistently(channel).ShouldNot(Receive())</pre>
<div class="blog_h3"><span class="graybg">修改默认间隔</span></div>
<p>默认情况下，Eventually每10ms轮询一次，持续1s。Consistently每10ms轮询一次，持续100ms。调用下面的函数修改这些默认值：</p>
<pre class="crayon-plain-tag">SetDefaultEventuallyTimeout(t time.Duration)
SetDefaultEventuallyPollingInterval(t time.Duration)
SetDefaultConsistentlyDuration(t time.Duration)
SetDefaultConsistentlyPollingInterval(t time.Duration)</pre>
<p>这些调用会影响整个测试套件。</p>
<div class="blog_h2"><span class="graybg">内置Matcher</span></div>
<div class="blog_h3"><span class="graybg">相等性</span> </div>
<pre class="crayon-plain-tag">// 使用reflect.DeepEqual进行比较
// 如果ACTUAL和EXPECTED都为nil，断言会失败
Ω(ACTUAL).Should(Equal(EXPECTED))

// 先把ACTUAL转换为EXPECTED的类型，然后使用reflect.DeepEqual进行比较
// 应当避免用来比较数字
Ω(ACTUAL).Should(BeEquivalentTo(EXPECTED))

// 使用 == 进行比较
BeIdenticalTo(expected interface{})</pre>
<div class="blog_h3"><span class="graybg">接口相容</span></div>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(BeAssignableToTypeOf(EXPECTED interface))</pre>
<div class="blog_h3"><span class="graybg">空值/零值</span></div>
<pre class="crayon-plain-tag">// 断言ACTUAL为Nil
Ω(ACTUAL).Should(BeNil())

// 断言ACTUAL为它的类型的零值，或者是Nil
Ω(ACTUAL).Should(BeZero())</pre>
<div class="blog_h3"><span class="graybg">布尔值</span></div>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(BeTrue())
Ω(ACTUAL).Should(BeFalse())</pre>
<div class="blog_h3"><span class="graybg">错误</span></div>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(HaveOccurred())

err := SomethingThatMightFail()
// 没有错误
Ω(err).ShouldNot(HaveOccurred())


// 如果ACTUAL为Nil则断言成功
Ω(ACTUAL).Should(Succeed())</pre>
<p>可以对错误进行细粒度的匹配：</p>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(MatchError(EXPECTED))</pre>
<p>上面的EXPECTED可以是：</p>
<ol>
<li>字符串：则断言ACTUAL.Error()与之相等</li>
<li>Matcher：则断言ACTUAL.Error()与之进行匹配</li>
<li>error：则ACTUAL和error基于reflect.DeepEqual()进行比较</li>
<li>实现了error接口的非Nil指针，调用<pre class="crayon-plain-tag">errors.As(ACTUAL, EXPECTED)</pre>进行检查</li>
</ol>
<p>不符合以上条件的EXPECTED是不允许的。</p>
<div class="blog_h3"><span class="graybg">通道</span></div>
<pre class="crayon-plain-tag">// 断言通道是否关闭
// Gomega会尝试读取通道进行判断，因此你需要注意：
//    如果是缓冲通道，你需要先将通道读干净
//    如果你后续需要再次读取通道，注意此断言的影响
Ω(ACTUAL).Should(BeClosed())
Ω(ACTUAL).ShouldNot(BeClosed())


// 断言能够从通道里面读取到消息
// 此断言会立即返回，如果通道已经关闭，则下面的断言失败
Ω(ACTUAL).Should(Receive(&lt;optionalPointer&gt;))



// 断言能够无阻塞的发送消息
Ω(ACTUAL).Should(BeSent(VALUE))</pre>
<div class="blog_h3"><span class="graybg">文件</span></div>
<pre class="crayon-plain-tag">// 文件或目录存在
Ω(ACTUAL).Should(BeAnExistingFile())
// 断言是普通文件
Ω(ACTUAL).Should(BeARegularFile())
// 断言是目录
BeADirectory</pre>
<div class="blog_h3"><span class="graybg">字符串</span></div>
<pre class="crayon-plain-tag">// 子串判断                        fmt.Sprintf(STRING, ARGS...)
Ω(ACTUAL).Should(ContainSubstring(STRING, ARGS...))

// 前缀判断
Ω(ACTUAL).Should(HavePrefix(STRING, ARGS...))

// 后缀判断
Ω(ACTUAL).Should(HaveSuffix(STRING, ARGS...))


// 正则式匹配
Ω(ACTUAL).Should(MatchRegexp(STRING, ARGS...))</pre>
<div class="blog_h3"><span class="graybg">JSON/XML/YML</span></div>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(MatchJSON(EXPECTED))
Ω(ACTUAL).Should(MatchXML(EXPECTED))
Ω(ACTUAL).Should(MatchYAML(EXPECTED))</pre>
<p>ACTUAL、EXPECTED可以是string、[]byte、Stringer。如果两者转换为对象是reflect.DeepEqual的则匹配。</p>
<div class="blog_h3"><span class="graybg">集合</span></div>
<p>string, array, map, chan, slice都属于集合。</p>
<pre class="crayon-plain-tag">// 断言为空
Ω(ACTUAL).Should(BeEmpty())

// 断言长度
Ω(ACTUAL).Should(HaveLen(INT))

// 断言容量
Ω(ACTUAL).Should(HaveCap(INT))

// 断言包含元素
Ω(ACTUAL).Should(ContainElement(ELEMENT))

// 断言等于                   其中之一
Ω(ACTUAL).Should(BeElementOf(ELEMENT1, ELEMENT2, ELEMENT3, ...))


// 断言元素相同，不考虑顺序
Ω(ACTUAL).Should(ConsistOf(ELEMENT1, ELEMENT2, ELEMENT3, ...))
Ω(ACTUAL).Should(ConsistOf([]SOME_TYPE{ELEMENT1, ELEMENT2, ELEMENT3, ...}))

// 断言存在指定的键，仅用于map
Ω(ACTUAL).Should(HaveKey(KEY))
// 断言存在指定的键值对，仅用于map
Ω(ACTUAL).Should(HaveKeyWithValue(KEY, VALUE))</pre>
<div class="blog_h3"><span class="graybg">数字/时间</span></div>
<pre class="crayon-plain-tag">// 断言数字意义（类型不感知）上的相等
Ω(ACTUAL).Should(BeNumerically("==", EXPECTED))

// 断言相似，无差不超过THRESHOLD（默认1e-8）
Ω(ACTUAL).Should(BeNumerically("~", EXPECTED, &lt;THRESHOLD&gt;))


Ω(ACTUAL).Should(BeNumerically("&gt;", EXPECTED))
Ω(ACTUAL).Should(BeNumerically("&gt;=", EXPECTED))
Ω(ACTUAL).Should(BeNumerically("&lt;", EXPECTED))
Ω(ACTUAL).Should(BeNumerically("&lt;=", EXPECTED))

Ω(number).Should(BeBetween(0, 10))</pre>
<p>比较时间时使用BeTemporally函数，和BeNumerically类似。 </p>
<div class="blog_h3"><span class="graybg">Panic</span></div>
<p>断言会发生Panic：</p>
<pre class="crayon-plain-tag">Ω(ACTUAL).Should(Panic())</pre>
<div class="blog_h3"><span class="graybg">And/Or</span></div>
<pre class="crayon-plain-tag">Expect(number).To(SatisfyAll(
            BeNumerically("&gt;", 0),
            BeNumerically("&lt;", 10)))
// 或者
Expect(msg).To(And(
            Equal("Success"),
            MatchRegexp(`^Error .+$`)))



Ω(ACTUAL).Should(SatisfyAny(MATCHER1, MATCHER2, ...))
// 或者
Ω(ACTUAL).Should(Or(MATCHER1, MATCHER2, ...))</pre>
<div class="blog_h2"><span class="graybg">自定义Matcher</span></div>
<p>如果内置Matcher无法满足需要，你可以实现接口：</p>
<pre class="crayon-plain-tag">type GomegaMatcher interface {
    Match(actual interface{}) (success bool, err error)
    FailureMessage(actual interface{}) (message string)
    NegatedFailureMessage(actual interface{}) (message string)
}</pre>
<div class="blog_h2"><span class="graybg">辅助工具</span></div>
<div class="blog_h3"><span class="graybg">ghttp</span></div>
<p>用于测试HTTP客户端，此包提供了Mock HTTP服务器的能力。</p>
<div class="blog_h3"><span class="graybg">gbytes</span></div>
<p>gbytes.Buffer实现了接口io.WriteCloser，能够捕获到内存缓冲的输入。配合使用<pre class="crayon-plain-tag">gbytes.Say</pre>能够对流数据进行有序的断言。</p>
<div class="blog_h3"><span class="graybg">gexec</span></div>
<p>简化了外部进程的测试，可以：</p>
<ol>
<li>编译Go二进制文件</li>
<li>启动外部进程</li>
<li>发送信号并等待外部进程退出</li>
<li>基于退出码进行断言</li>
<li>将输出流导入到gbytes.Buffer进行断言</li>
</ol>
<div class="blog_h3"><span class="graybg">gstruct</span></div>
<p>此包用于测试复杂的Go结构，提供了结构、切片、映射、指针相关的Matcher。</p>
<p>对所有字段进行断言：</p>
<pre class="crayon-plain-tag">actual := struct{
    A int
    B bool
    C string
}{5, true, "foo"}
Expect(actual).To(MatchAllFields(Fields{
    "A": BeNumerically("&lt;", 10),
    "B": BeTrue(),
    "C": Equal("foo"),
})</pre>
<p>不处理某些字段： </p>
<pre class="crayon-plain-tag">Expect(actual).To(MatchFields(IgnoreExtras, Fields{
    "A": BeNumerically("&lt;", 10),
    "B": BeTrue(),
    // 忽略C字段
})


Expect(actual).To(MatchFields(IgnoreMissing, Fields{
    "A": BeNumerically("&lt;", 10),
    "B": BeTrue(),
    "C": Equal("foo"),
    "D": Equal("bar"), // 忽略多余字段
})</pre>
<p>一个复杂的例子：</p>
<pre class="crayon-plain-tag">coreID := func(element interface{}) string {
    return strconv.Itoa(element.(CoreStats).Index)
}
Expect(actual).To(MatchAllFields(Fields{
    // 忽略此字段
    "Name":      Ignore(),
    // 时间断言
    "StartTime": BeTemporally("&gt;=", time.Now().Add(-100 * time.Hour)),
    //     解引用后再断言
    "CPU": PointTo(MatchAllFields(Fields{
        "Time":                 BeTemporally("&gt;=", time.Now().Add(-time.Hour)),
        "UsageNanoCores":       BeNumerically("~", 1E9, 1E8),
        "UsageCoreNanoSeconds": BeNumerically("&gt;", 1E6),
        //       包含匹配的元素， 抽取ID的函数
        "Cores": MatchElements(coreID, IgnoreExtras, Elements{
            // ID: Matcher
            "0": MatchAllFields(Fields{
                Index: Ignore(),
                "UsageNanoCores":       BeNumerically("&lt;", 1E9),
                "UsageCoreNanoSeconds": BeNumerically("&gt;", 1E5),
            }),
            "1": MatchAllFields(Fields{
                Index: Ignore(),
                "UsageNanoCores":       BeNumerically("&lt;", 1E9),
                "UsageCoreNanoSeconds": BeNumerically("&gt;", 1E5),
            }),
        }),
    }))
    "Logs":               m.Ignore(),
}))</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ginkgo-study-note">Ginkgo学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ginkgo-study-note/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Python单元测试</title>
		<link>https://blog.gmem.cc/python-unit-test</link>
		<comments>https://blog.gmem.cc/python-unit-test#comments</comments>
		<pubDate>Wed, 16 Oct 2019 05:51:14 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Python]]></category>
		<category><![CDATA[单元测试]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=29327</guid>
		<description><![CDATA[<p>unittest 简介 属于标准库的一部分，类似于JUnit。下面是一个基本的例子： [crayon-69e29c2da804a797988397/] 从这个例子可以看到： 测试用例以类的形式进行分组，从[crayon-69e29c2da804f191831720-i/]继承 测试方法以[crayon-69e29c2da8051440454738-i/]开头 unittest支持类似于JUnit的准备/清理机制 单元测试的入口点均为unittest.main() unittest.TestCase提供了一系列断言方法 跳过测试 可以在测试类、测试方法上添加装饰器，以便在特定条件下，跳过某些测试： [crayon-69e29c2da8053274320502/] 命令行 基本用法 你可以运行测试模块、测试类，甚至测试方法： [crayon-69e29c2da8055962031447/] 自动搜索用例 如果要自动搜索测试用例，执行：  [crayon-69e29c2da8057178374454/] 要支持自动搜索，测试用例必须编写为模块、或者包，且可以从项目根目录导入。  默认情况下，仅仅需要命名为[crayon-69e29c2da805a364572695-i/]的文件，可以通过[crayon-69e29c2da805c459438578-i/]参数修改此行为。 <a class="read-more" href="https://blog.gmem.cc/python-unit-test">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/python-unit-test">Python单元测试</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">unittest</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>属于标准库的一部分，类似于JUnit。下面是一个基本的例子：</p>
<pre class="crayon-plain-tag"># 编写测试用例模块
import util        # 被测试模块
import unittest
class TestUtilFunc(unittest.TestCase):
    def setUp(self): pass     # 每个测试函数运行之前
    def tearDown(self): pass  # 每个测试函数运行之后
    def test_indexOf(self):    # 测试用例
        self.assertEqual(0,util.stringutils.indexOf('123','1'))

# 运行单元测试
if __name__ == '__main__'
    unittest.main()</pre>
<p>从这个例子可以看到：</p>
<ol>
<li>测试用例以类的形式进行分组，从<pre class="crayon-plain-tag">unittest.TestCase</pre>继承</li>
<li>测试方法以<pre class="crayon-plain-tag">test_</pre>开头</li>
<li>unittest支持类似于JUnit的准备/清理机制</li>
<li>单元测试的入口点均为unittest.main()</li>
<li>unittest.TestCase提供了一系列断言方法</li>
</ol>
<div class="blog_h2"><span class="graybg">跳过测试</span></div>
<p>可以在测试类、测试方法上添加装饰器，以便在特定条件下，跳过某些测试：</p>
<pre class="crayon-plain-tag"># 无条件跳过
@unittest.skip("reason")

# 条件跳过
@unittest.skipIf( True, "reason" )
@unittest.skipUnless(sys.platform.startswith("win"), "reason") </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"># 测试两个模块
python -m unittest test_module1 test_module2
# 测试一个类
python -m unittest test_module.TestClass
# 测试一个方法
python -m unittest test_module.TestClass.test_method

# 也可以指定模块的路径
python -m unittest tests/test_something.py</pre>
<div class="blog_h3"><span class="graybg">自动搜索用例</span></div>
<p>如果要自动搜索测试用例，执行： </p>
<pre class="crayon-plain-tag">cd project_directory
python -m unittest discover</pre>
<p>要支持自动搜索，测试用例<span style="background-color: #c0c0c0;">必须编写为模块、或者包，且可以从项目根目录导入</span>。 </p>
<p>默认情况下，仅仅需要命名为<pre class="crayon-plain-tag">test*.py</pre>的文件，可以通过<pre class="crayon-plain-tag">-p</pre>参数修改此行为。</p>
<div class="blog_h1"><span class="graybg">pytest</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>pytest是第三方框架，和unittest的主要区别是：</p>
<ol>
<li>测试模块的文件名必须以<pre class="crayon-plain-tag">test_</pre>开头或<pre class="crayon-plain-tag">_test</pre>结尾</li>
<li>测试类名必须以<pre class="crayon-plain-tag">Test</pre>开头</li>
<li>支持模块、类、函数级别的准备/清理方法，unittest仅支持类级别</li>
<li>不提供断言方法，直接使用assert表达式</li>
<li>支持失败用例的重跑</li>
</ol>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">pip install -U pytest</pre>
<div class="blog_h2"><span class="graybg">示例</span></div>
<div class="blog_h3"><span class="graybg">第一个用例 </span></div>
<pre class="crayon-plain-tag"># 被测试者
def func(x):
    return x + 1


def test_answer():
    # 断言
    assert func(3) == 5</pre>
<p>运行<pre class="crayon-plain-tag">pytest</pre>即可执行测试。 不带任何参数表示，<span style="background-color: #c0c0c0;">递归的寻找当前目录下test_*.py和*_test.py</span>文件并执行其中定义的测试。</p>
<div class="blog_h3"><span class="graybg">测试分组</span></div>
<p>可以将多个测试用例组合为类：</p>
<pre class="crayon-plain-tag">class TestClass:
    def test_one(self):
        x = "this"
        assert "h" in x

    def test_two(self):
        x = "hello"
        assert hasattr(x, "check")</pre>
<p>运行<pre class="crayon-plain-tag">pytest -q test_class.py</pre>表示仅仅测试上面这个文件。</p>
<div class="blog_h3"><span class="graybg">断言异常发生</span></div>
<pre class="crayon-plain-tag">import pytest


def f():
    raise SystemExit(1)


def test_mytest():
    # 断言f()调用会产生SystemExit异常
    with pytest.raises(SystemExit):
        f()</pre>
<div class="blog_h3"><span class="graybg">使用fixture</span></div>
<p>测试方法中的参数tmpdir提示系统，自动为此测试创建一个独特的临时目录。</p>
<pre class="crayon-plain-tag">def test_needsfiles(tmpdir):
    print(tmpdir)
    assert 0</pre>
<div class="blog_h2"><span class="graybg">准备/清理</span></div>
<p>pytest支持不同级别的setup/teardown方法。</p>
<div class="blog_h3"><span class="graybg">模块级别</span></div>
<p>整个模块仅仅调用一次：</p>
<pre class="crayon-plain-tag">def setup_module(module):
    """ setup any state specific to the execution of the given module."""


def teardown_module(module):
    """ teardown any state that was previously setup with a setup_module
    method.
    """</pre>
<div class="blog_h3"><span class="graybg">类级别</span></div>
<p>对于类中的所有测试，调用一次：</p>
<pre class="crayon-plain-tag">@classmethod
def setup_class(cls):
    """ setup any state specific to the execution of the given class (which
    usually contains tests).
    """


@classmethod
def teardown_class(cls):
    """ teardown any state that was previously setup with a call to
    setup_class.
    """</pre>
<div class="blog_h3"><span class="graybg">方法级别</span></div>
<p>对于每个测试方法，都会调用：</p>
<pre class="crayon-plain-tag">def setup_method(self, method):
    """ setup any state tied to the execution of the given method in a
    class.  setup_method is invoked for every test method of a class.
    """


def teardown_method(self, method):
    """ teardown any state that was previously setup with a setup_method
    call.
    """ </pre>
<div class="blog_h2"><span class="graybg">fixtures</span></div>
<p>pytest支持fixtures，所谓fixtures是一系列让测试可靠、可重复执行的机制。比起经典的xUnit风格的setup/teardown函数，pytest fixtures具有以下优势：</p>
<ol>
<li>每个fixture具有精确的名称，通过在测试函数、模块、类，或者整个项目中声明这些名称即可自动激活</li>
<li>fixture使用模块化设计，每个fixture名称会触发一个<span style="background-color: #c0c0c0;">fixture</span>函数，此函数本身<span style="background-color: #c0c0c0;">亦可使用其它fixture</span></li>
<li>fixture可以在简单的单元测试场景，到复杂的功能测试中使用</li>
<li>你可以根据配置、组件选项来参数化fixture和测试</li>
<li>可以跨越函数、类、模块、整个测试会话重用fixture</li>
</ol>
<div class="blog_h3"><span class="graybg">内置fixture列表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">Fixture</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>cache</td>
<td>返回一个缓存对象，可以跨越多个测试session共享数据：<br />
<pre class="crayon-plain-tag"># key是由 / 分隔的字符串，通常第一部分是你的插件/应用的名称，避免和其它缓存用户冲突
cache.get(key, default)
cache.set(key, value) </pre>
</td>
</tr>
<tr>
<td>capsys</td>
<td>启用对输出到sys.stdout / sys.stderr中的文本的捕获，调用<pre class="crayon-plain-tag">capsys.readouterr()</pre>返回命名元组<pre class="crayon-plain-tag">(out ,err)</pre></td>
</tr>
<tr>
<td>capsysbinary</td>
<td>类似上面，但是捕获的是byte而非text</td>
</tr>
<tr>
<td>tmpdir_factory [session scope]</td>
<td>返回临时目录工厂</td>
</tr>
<tr>
<td>tmp_path_factory [session scope]</td>
<td>返回临时路径工厂</td>
</tr>
<tr>
<td>tmpdir</td>
<td>返回一个临时目录对象</td>
</tr>
<tr>
<td>tmp_path</td>
<td>返回一个临时目录路径对象</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">注册fixture</span></div>
<p>Fixture由函数创建，任何函数加上<pre class="crayon-plain-tag">@pytest.fixture</pre>即可创建Fixture：</p>
<pre class="crayon-plain-tag">@pytest.fixture
def smtp_connection():
    import smtplib
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5) </pre>
<p>你可以指定Fixture的共享范围：</p>
<pre class="crayon-plain-tag"># 可选值 function, class, module, package, session
@pytest.fixture(scope="module") </pre>
<div class="blog_h3"><span class="graybg">作为函数参数</span></div>
<p>直接将Fixture函数名作为参数，声明在被测试用例的参数列表中，即可使用Fixture： </p>
<pre class="crayon-plain-tag">def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250 </pre>
<div class="blog_h3"><span class="graybg">清理fixture</span></div>
<p>在你的Fixture函数中使用yield语句，即可自动在Fixture超过作用域范围后自动清理： </p>
<pre class="crayon-plain-tag">@pytest.fixture(scope="module")
def smtp_connection():
    smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
    # 提供
    yield smtp_connection
    print("teardown smtp")
    # 清理
    smtp_connection.close()


# 等价形式
@pytest.fixture(scope="module")
def smtp_connection():
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
        yield smtp_connection</pre>
<div class="blog_h3"><span class="graybg">参数化fixture</span></div>
<p>使用参数化fixture，可以让所有依赖于此fixture的测试运行多次：</p>
<pre class="crayon-plain-tag">import pytest
import smtplib

@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
    smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
    yield smtp_connection
    print("finalizing {}".format(smtp_connection))
    smtp_connection.close()</pre>
<p>使用smtp_connection的用例会执行两次，一次连接到stmp服务器smtp.gmail.com，另一次连接到mail.python.org。</p>
<div class="blog_h3"><span class="graybg">读取上下文</span></div>
<p>为Fixture函数传入<pre class="crayon-plain-tag">request</pre>对象，则可以动态获取当前被测试函数、类、模块的上下文信息：</p>
<pre class="crayon-plain-tag">import pytest
import smtplib


@pytest.fixture(scope="module")
def smtp_connection(request):
    # 读取被测试模块的smtpserver属性
    server = getattr(request.module, "smtpserver", "smtp.gmail.com")
    smtp_connection = smtplib.SMTP(server, 587, timeout=5)
    yield smtp_connection
    print("finalizing {} ({})".format(smtp_connection, server))
    smtp_connection.close()</pre>
<p>被测试模块可以提供stmpserver变量供Fixture读取：</p>
<pre class="crayon-plain-tag">smtpserver = "mail.python.org"  # 被Fixture读取

def test_showhelo(smtp_connection):
    assert 0, smtp_connection.helo()</pre>
<div class="blog_h3"><span class="graybg">Fixture工厂</span></div>
<p>如果在单个测试中，需要多次得到全新的fixture，可以让Fixture函数返回一个函数而不是值： </p>
<pre class="crayon-plain-tag">@pytest.fixture
def make_customer_record():
    def _make_customer_record(name):
        return {"name": name, "orders": []}
    # 此Fixture函数返回一个工厂函数
    return _make_customer_record


def test_customer_records(make_customer_record):
                 # 可以多次生成fixture
    customer_1 = make_customer_record("Lisa")
    customer_2 = make_customer_record("Mike")
    customer_3 = make_customer_record("Meredith")</pre>
<div class="blog_h3"><span class="graybg">在类/模块/项目级别启用</span></div>
<p>在类级别启用fixture：</p>
<pre class="crayon-plain-tag">@pytest.fixture()
def cleandir():
    newpath = tempfile.mkdtemp()
    os.chdir(newpath)


# 所有测试都会在一个临时目录下执行
@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit:</pre>
<p>支持同时使用多个fixture：</p>
<pre class="crayon-plain-tag">@pytest.mark.usefixtures("cleandir", "anotherfixture")</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/python-unit-test">Python单元测试</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/python-unit-test/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-69e29c2da8953822843226-i/]命令组成。 要编写测试用例，你需要创建一个以 [crayon-69e29c2da8958414610935-i/]结尾的源文件。该文件中包含一个或多个如下签名的函数： [crayon-69e29c2da895a696078067/] 如果想让测试失败，调用testing.T的方法： [crayon-69e29c2da895c366152432/] 基准测试 testing包还支持性能基准测试。要执行基准测试，调用命令[crayon-69e29c2da895e696015177-i/]。 基准测试方法的签名如下： [crayon-69e29c2da8960015117625/] 如果要基准测试并发执行的性能，可以使用[crayon-69e29c2da8962051050387-i/]标记，并且调用助手函数：  [crayon-69e29c2da8964573984888/] 验证样例  testing包还支持执行并验证样例代码。在被验证方法中，你可以通过注释声明期望的标准输出。 如果被验证方法的标准输出和注释匹配（不考虑首尾空白符），则验证通过。 示例： [crayon-69e29c2da8967613140056/] 没有期望输出注释的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-69e29c2da9308881754052/] 假设你已经实现了服务AccountService： [crayon-69e29c2da930d051062731/] 而你的搭档负责的PersonService还没有开发完毕，如何方便的进行单元测试呢？ 在JUnit中集成Spring上下文的支持 你可能会觉得，我们不需要在单元测试中引入Spring。对于上面的例子的确可以这么说，它太简单了，AccountServiceImpl 依赖的PersonService完全可以通过setter手工注入。但是实际的开发场景要比这个例子复杂的多，待测试类可能和Spring管理的Beans存在很多关联，它可能依赖于Spring提供的数据源、事务管理器，等等。这些Bean如果都手工管理，将是相当繁琐无味的工作。 使用JUnit 4.x提供的注解[crayon-69e29c2da9310426931866-i/] ，可以指定单元测试的“运行类”，运行类必须继承自[crayon-69e29c2da9312843697184-i/] 并实现[crayon-69e29c2da9314739676119-i/] 方法。Spring Test框架提供的运行类是[crayon-69e29c2da9316752266360-i/] ，使用该类可以轻松的将Spring和JUnit进行集成。该类的用法示例如下： [crayon-69e29c2da9318176163004/] AccountService的测试用例 依据上一节的知识，我们编写集成Spring Test的测试用例： [crayon-69e29c2da931a041921332/] 基于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-69e29c2da9aff927708777/] 可以看到，都Mock的每一种调用，我们都可以监控到；相应的Mock可以迅速、准确的模拟出真实对象的行为， 而忽略任何行为的实现细节。 未打桩前，Mock这样处理返回值： 返回Object的方法，Mock默认返回null 返回集合的方法，Mock默认返回空集合 返回数字的方法，Mock默认返回0 返回布尔的方法，Mock默认返回false 对Mock的打桩可以进行多次，后面的会覆盖前面的打桩。 参数匹配 对Mock进行打桩或者验证时时，可以有多种匹配入参的方式： [crayon-69e29c2da9b03066222333/] 参数捕获 <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>
