Golang学习笔记(四)Ginkgo测试框架

本文是Ginkgo测试框架的学习笔记,涵盖了安装、框架解析、Specs构建以及Spec Runner等内容。讲解了如何使用Ginkgo进行Bootstrapping、Adding Specs、Before/BeforeEach等操作,并介绍了如何标记失败的测试用例和记录输出。
该文章已生成可运行项目,

Ginkgo 测试框架学习笔记

链接导航 :
Ginkgo主页
Github源码

一、安装Ginkgo

$ go get github.com/onsi/ginkgo/ginkgo
$ go get github.com/onsi/gomega/...

然后会有一个ginkgo.exe安装在GOPATH路径的bin里
在这里插入图片描述

二、框架拆解

Ginkgo 提供了一些example

1、Bootstrapping a Suite

$ cd path/to/books
$ ginkgo bootstrap
package books_test

import (
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
    "testing"
)

func TestBooks(t *testing.T) {
    RegisterFailHandler(Fail)
    RunSpecs(t, "Books Suite")
}
  1. 新建两个package books_test 进行测试,一个文件用于写测试用例,一个文件放启动测试套件的函数.
  2. 导入Ginkgo和Gomega包时在包名前加一个[.],这样可以直接使用包中的方法函数,不用再【包名.函数名】
  3. 执行文件的时候,可以用go test方法也可以用ginkgo
  4. RegisterFailHandler(Fail): ginkgo通过调用Fail(description string)函数来发出fail信号。我们用RegisterFailHandler() 将Fail函数传递给Gomega。RegisterFailHandler()是连接ginkgo和gomega的唯一途径。
  5. RunSpecs(t *testing.T, suiteDescription string):用于启动测试套件,如果任何一个specs失败,该套件则自动返回失败。

执行结果

$ ginkgo #or go test

=== RUN TestBootstrap

Running Suite: Books Suite
==========================
Random Seed: 1378936983

Will run 0 of 0 specs


Ran 0 of 0 Specs in 0.000 seconds
SUCCESS! -- 0 Passed | 0 Failed | 0 Pending | 0 Skipped

--- PASS: TestBootstrap (0.00 seconds)
PASS
ok      books   0.019s

2、Adding Specs to a Suite

$ ginkgo generate book
package books_test

import (
    "/path/to/books"
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
)

var _ = Describe("Book", func() {
	 var (
        longBook  Book
        shortBook Book
    )

    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("should be a novel", func() {
                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"))
            })
        })
    })
})
  1. 我们添加了一个顶层的描述容器,用Ginkgo的【Describe(text string, body func() ) bool 】函数。使用【var _ = …】允许我们直接描述填充顶层的Describe() 不需要用【func init(){}】初始化包装它。
  2. 【Describe() {}】中的函数包含了我们的使用规范。
  3. 为了共享BeforeEach和It之间的状态,Ginkgo使用闭包函数变量来构建顶层的Describe()和Context()
  4. Ginkgo中使用【Descirbe()】和【Context()】来描述代码的测试行为,将一个或多个测试用例It归类。
  5. Ginkgo中使用【BeforceEach()】来为specs设置状态,执行每个测试用例It前执行该段代码,使用【It()】来指定单个spec,是测试用例的基本单位,即It中包含的代码就算一个测试用例。
  6. 使用Gomega中的【Expect()】函数来设置期望。

执行结果

$ ginkgo # or go test
=== RUN TestBootstrap

Running Suite: Books Suite
==========================
Random Seed: 1378938274

Will run 2 of 2 specs

••
Ran 2 of 2 Specs in 0.000 seconds
SUCCESS! -- 2 Passed | 0 Failed | 0 Pending | 0 Skipped

--- PASS: TestBootstrap (0.00 seconds)
PASS
ok      books   0.025s

3、Marking Specs as Failed

虽然通常使用匹配器库(如Gomega)在规范中进行断言,但Ginkgo提供了一个简单的、全局的失败函数,它允许您将一个spec标记为失败。

Fail("Failure reason")

Fail() 标志该测试用例运行结果为失败,并打印里面的信息。
Fail和Gomega(Gomega也调用了Fail)将会把当前的Space和panic记录为失败。同时Ginkgo会停止当前进度中的spec,包括后续所有与该时间相关的断言和代码。
通常Ginkgo会自动挽救这种panic,并进入下一段测试。
如果测试启动的一个线程调用了Fail,或调用了Gomega的失败断言,那么Ginkgo没有办法挽救错误引起的panic。这会导致后续的测试内容中断。为了避免这种情况,需要使用【GinkgoRecover】来挽救panic。
Example:

It("panics in a goroutine", func(done Done) {
    go func() {
        defer GinkgoRecover()

        Ω(doSomething()).Should(BeTrue())

        close(done)
    }()
})

4、Logging Output

Ginkgo提供了一个全局的io.Writter 叫作GinkgoWtiter。当测试运行时可以向GinkgoWriter进行输入,只有测试失败时,才会将它输出stdout。当运行在详细模式(ginkgo -v或go test -ginkgo.v) GinkgoWriter总是立即重定向它的输入到stdout。
当Ginkgo测试套件中断(通过 ^C),Ginkgo会将所有的内容写入GinkgoWriter。便于调试测试用例。

三、构建Specs

1、独立Specs:It()

可以向Describe() 或 Context() 中添加It 模块。

var _ = Describe("Book", func() {
    It("can be loaded from JSON", func() {
        book := NewBookFromJSON(`{
            "title":"Les Miserables",
            "author":"Victor Hugo",
            "pages":1488
        }`)

        Expect(book.Title).To(Equal("Les Miserables"))
        Expect(book.Author).To(Equal("Victor Hugo"))
        Expect(book.Pages).To(Equal(1488))
    })
})

2、Specify 别名

Specify和It功能完全一样,It属于其简写。 为了能够让测试用例Specs更容易阅读,Specify, PSpecify, XSpecify, 和 FSpecify 作为别名使用,应对It 不能作为自然语言读取的情况。

Describe("The foobar service", func() {
  Context("when calling Foo()", func() {
    Context("when no ID is provided", func() {
      Specify("an ErrNoID error is returned", func() {
      })
    })
  })
})

3、合并共用设置:BeforeEach()

使用BeforeEach() 共享模块的共用设置:

var _ = Describe("Book", func() {
    var book Book

    BeforeEach(func() {
        book = NewBookFromJSON(`{
            "title":"Les Miserables",
            "author":"Victor Hugo",
            "pages":1488
        }`)
    })

    It("can be loaded from JSON", func() {
        Expect(book.Title).To(Equal("Les Miserables"))
        Expect(book.Author).To(Equal("Victor Hugo"))
        Expect(book.Pages).To(Equal(1488))
    })

    It("can extract the author's last name", func() {
        Expect(book.AuthorLastName()).To(Equal("Hugo"))
    })
})

BeforeEach在每个Specs之前运行,从而确保每个Specs都有原始的State 副本。使用闭包的函数变量(在本例中为var book book)共享测试用例的初始状态。您还可以通过AfterEach模块执行清理。
Tips:当有多层嵌套关系时,BeforeEach会在包含该BeforeEach的 Describe下 所有的It执行前执行:

Describe("test",func(){
	Context("test_c",func(){
		BeforEarch(func(){
		fmt.Println("this is a beforeEach")
		})
		It("test_I",func(){
			fmt.Println("第一个IT")
		})
	})
	Context("test_c2",func(){
		It("test_I2",func(){
			fmt.Println("第二个IT")
		})
	})
})
//输入结果是: 
this is a beforeEach
第一个IT
this is a beforeEach
第二个IT

不管这个BeforeEach是在哪个Context下面,只要是在该Describe下的It(),都会执行该BeforeEach ,所以BeforeEach不要放在Context中,声明时要放在Describe下面.

通常我们还会在BeforeEach和AfterEach中加入断言。 这些断言可以判断在为测试用例准备State的时候有无发生错误。

4、通过容器整理测试用例Spec:Describe() and Context()

var _ = Describe("Book", func() {
    var (
        book Book
        err error
    )

    BeforeEach(func() {
        book, err = NewBookFromJSON(`{
            "title":"Les Miserables",
            "author":"Victor Hugo",
            "pages":1488
        }`)
    })

    Describe("loading from JSON", func() {
        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())
            })
        })

        Context("when the JSON fails to parse", func() {
            BeforeEach(func() {
                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"))
        })
    })
})

使用Describe()来描述这段代码的行为,使用Context()来描述表达该行为在不同的环境下执行。在例子中,第一个测试行为Describe()是要导入json,测试用例有两种情景,一种是导入成功,一种是导入失败。
当Describe/Context 嵌套BeforeEach和AfterEach时,执行每一个It模块,都会执行一次BeforeEach和AfterEach,以确保每个Specs都处于原始状态。
不要写嵌套写Context,只有在最后写测试用例It()时,才再前面写Context, 因为Context没有分割测试用例的能力,Describe可以分割,确定BeforeEach和AfterEach的执行范围.

通常,容器块中的代码应该是It块或BeforeEach/JustBeforeEach/JustAfterEach/AfterEach块,
或闭包变量声明。在容器块中断言通常是错误的。
在容器块中初始化闭包变量也是错误的。如果您的Its中的一个改变了该变量,随后的Its将接收改变后的值。
这是一个测试污染的案例,很难追踪。总是在每个块之前初始化变量。

如果你想在运行时获得关于当前测试的信息,你可以使用CurrentGinkgoTestDescription()从任何It或BeforeEach/JustBeforeEach/JustAfterEach/AfterEach块。这个调用返回的CurrentGinkgoTestDescription具有关于当前运行的测试的各种信息,包括文件名、行号、It块中的文本以及周围容器块中的文本。

5、分离创建和配置:JustBeforeEach

JustBeforeEach() 模块在所有BeforeEach模块执行之后,It模块执行之前运行。所以,可以在BeforeEach()中创建配置参数,在JustBeforeEach()中进行导入和创建。
这样可以将It 执行前的条件配置关系更加多元化,减少重复代码。

# 未加入BeforeEach
BeforeEach(func() {
        book, err = NewBookFromJSON(`{
            "title":"Les Miserables",
            "author":"Victor Hugo",
            "pages":1488
        }`)
})
    
# 加入JustBeforeEach
BeforeEach(func() {
        json = `{
            "title":"Les Miserables",
            "author":"Victor Hugo",
            "pages":1488
        }`
})
JustBeforeEach(func() {
    book, err = NewBookFromJSON(json)
})

Describe("loading from JSON", func() {
        Context("when the JSON fails to parse", func() {
            BeforeEach(func() {
                json = `{
                    "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())
            })
        })
})
在不同的嵌套级别上,可以有多个JustBeforeEaches。
Ginkgo会先从外跑所有的BeforeEach,然后它会从外跑所有的JustBeforeEach。
虽然这个功能强大,但这可能会导致测试套件的混乱,所以要明智地嵌套使用JustBeforeEach。

6、全局设置和卸载:BeforeSuite 和AfterSuite

有时您希望在执行整个测试套件之前运行一些代码,或者完成整个测试套件之后运行一些代码。例如,可能需要启动和关闭外部数据库。
Ginkgo提供了BeforeSuite 和AfterSuite来实现这一点。

package books_test

import (
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"

    "your/db"

    "testing"
)

var dbRunner *db.Runner
var dbClient *db.Client

func TestBooks(t *testing.T) {
    RegisterFailHandler(Fail)

    RunSpecs(t, "Books Suite")
}

var _ = BeforeSuite(func() {
    dbRunner = db.NewRunner()
    err := dbRunner.Start()
    Expect(err).NotTo(HaveOccurred())

    dbClient = db.NewClient()
    err = dbClient.Connect(dbRunner.Address())
    Expect(err).NotTo(HaveOccurred())
})

var _ = AfterSuite(func() {
    dbClient.Cleanup()
    dbRunner.Stop()
})

BeforeSuite函数在所有Specs运行前执行,如果BeforeSuite报错,则后续测试套件不会执行。
AfterSuite函数在所有Specs运行后执行,不论测试是否失败。因为AfterSuite通常包含清理现有状态的代码,所以当你向运行中的测试套件发送一个中断信号(^C)时,Ginkgo也会执行AfterSuite。要中止AfterSuite,需要发送另一个中断信号。
BeforeSuite和AfterSuite都只能创建一次。
并行运行时,运行每个进程的前后都会执行一次BeforeSuite和AfterSuite。

7、记录复杂的It :By()

作为一个测试规则,尽量让It() 、BeforeEach() 等描述内容简短且切中要点。在复杂测试流程中,使用By来提供辅助描述:

var _ = Describe("Browsing the library", func() {
    BeforeEach(func() {
        By("Fetching a token and logging in")

        authToken, err := authClient.GetToken("gopher", "literati")
        Exepect(err).NotTo(HaveOccurred())

        err := libraryClient.Login(authToken)
        Exepect(err).NotTo(HaveOccurred())
    })

    It("should be a pleasant experience", func() {
        By("Entering an aisle")

        aisle, err := libraryClient.EnterAisle()
        Expect(err).NotTo(HaveOccurred())

        By("Browsing for books")

        books, err := aisle.GetBooks()
        Expect(err).NotTo(HaveOccurred())
        Expect(books).To(HaveLen(7))

        By("Finding a particular book")

        book, err := books.FindByTitle("Les Miserables")
        Expect(err).NotTo(HaveOccurred())
        Expect(book.Title).To(Equal("Les Miserables"))

        By("Check the book out")

        err := libraryClient.CheckOut(book)
        Expect(err).NotTo(HaveOccurred())
        books, err := aisle.GetBooks()
        Expect(books).To(HaveLen(6))
        Expect(books).NotTo(ContainElement(book))
    })
})

By()中的字符串会传递给GinkgoWriter。如果测试成功,则不会有显示。测试失败时,可以打印出每个步骤直到失败前。使用ginkgo -v时,则无论成功失败都会打印详细信息。

四、The Spec Runner

1、 Pending Specs

您可以将单个Spec或容器标记为pending。这将阻止spec(或容器内的spec)运行。在Describe, Context, It, and Measure前面加一个P或X:

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")
你不需要移除func(){..} 当您将一个It或Measure标记为pending时。
Ginkgo会忽略字符串后面的任何参数。
默认情况下,Ginkgo会为每个未完成的Spec打印一个描述,
你可以通过设置--noisyPendings=false 来阻止它。
默认情况下,Ginkgo不会因为有待定Specs而失败。
您可以传递--failOnPending 来反转此行为。

2、focus Specs

F含义Focus,使用后表示只执行该模块包含的测试:

 FDescribe("some behavior", func() { ... })
 FContext("some scenario", func() { ... })
 FIt("some assertion", func() { ... })

当里层和外层都存在Focus时,外层的无效,即下面代码只会执行B测试用例:

FDescribe("outer describe", func() {
    It("A", func() { ... })
    FIt("B", func() { ... })
})
本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值