Ginkgo 编写规格

Ginkgo使得编写富有表现力的规格变得容易,这些规格以有组织的方式描述代码的行为。我们已经看到Ginkgo套件是规格的分层集合,由容器节点、设置节点和主题节点组成,组织成规格树。在本节中,我们将深入探讨Ginkgo中可用的各种节点及其属性。

规格中的主题:It

每个Ginkgo规格恰好有一个主题节点。可以通过添加一个新的主题节点使用It(<description>, <closure>)来向套件添加一个单独的规格。这里有一个规格来验证我们可以从Book模型中提取作者的姓氏:

var _ = Describe("Books", func() {
  It("can extract the author's last name", func() {
    book = &books.Book{
      Title: "Les Miserables",
      Author: "Victor Hugo",
      Pages: 2783,
    }

    Expect(book.AuthorLastName()).To(Equal("Hugo"))
  })
})

如你所见,描述记录了规格的意图,而闭包包含了关于我们代码行为的断言。

可以向Describe容器添加多个规格:

var _ = Describe("Books", func() {
  It("can extract the author's last name", func() {
    book = &books.Book{
      Title: "Les Miserables",
      Author: "Victor Hugo",
      Pages: 2783,
    }

    Expect(book.AuthorLastName()).To(Equal("Hugo"))
  })

  It("can fetch a summary of the book from the library service", func(ctx SpecContext
   {
    book = &books.Book{
      Title: "Les Miserables",
      Author: "Victor Hugo",
      Pages: 2783,
    }

    summary, err := library.FetchSummary(ctx, book)
    Expect(err).NotTo(HaveOccurred())
    Expect(summary).To(ContainSubstring("Jean Valjean"))
  }, SpecTimeout(time.Second))
})

新规格连接到一个图书馆服务来获取书籍的摘要,并断言请求成功并且得到了有意义的响应。这个例子预览了一些你将在这些文档后面学到的高级概念:Ginkgo支持装饰器SpecTimeout来注释和修改规格的行为;Ginkgo允许你通过编写接受SpecContextcontext.Context可中断规格来测试可能长时间运行的代码。现在,如果超过一秒钟,或者接收到中断信号,Ginkgo将会发出信号让library.FetchSummary清理通过取消ctx

Ginkgo提供了一个It的别名叫做SpecifySpecify在功能上与It完全相同,但它可以帮助你的规格读起来更自然。

提取公共设置:BeforeEach

可以通过BeforeEach(<closure>)设置节点来移除重复并共享跨规格的公共设置。让我们向我们的Book套件添加规格,涵盖提取作者的名字和一些自然边缘情况:

var _ = Describe("Books", func() {
  var book *books.Book

  BeforeEach(func() {
    book = &books.Book{
      Title: "Les Miserables",
      Author: "Victor Hugo",
      Pages: 2783,
    }
    Expect(book.IsValid()).To(BeTrue())
  })

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

  It("interprets a single author name as a last name", func() {
    book.Author = "Hugo"
    Expect(book.AuthorLastName()).To(Equal("Hugo"))
  })

  It("can extract the author's first name", func() {
    Expect(book.AuthorFirstName()).To(Equal("Victor"))
  })

  It("returns no first name when there is a single author name", func() {
    book.Author = "Hugo"
    Expect(book.AuthorFirstName()).To(BeZero()) //BeZero断言该值是其类型的零值。在这种情况下:""
  })
})

现在有四个主题节点,所以Ginkgo将运行四个规格。每个规格的共同设置被捕获在BeforeEach节点中。当运行每个规格时,Ginkgo将首先运行BeforeEach闭包,然后是主题闭包。

信息通过闭包变量在闭包之间共享。按照惯例,这些闭包变量应该在容器节点闭包内声明并在设置节点闭包内初始化。这样做确保每个规格都有一个纯净的、正确初始化的共享变量副本。

在这个例子中,single author name规格改变了共享的book闭包变量。这些变化不会污染其他规格,因为BeforeEach闭包重新初始化了book

这个细节非常重要。Ginkgo默认要求规格必须是完全独立的。这允许Ginkgo打乱规格的顺序并且并行运行规格。我们稍后将更详细地讨论这一点,但现在要记住这个要点:“在容器节点中声明,在设置节点中初始化”

最后一点 - Ginkgo允许在设置节点和主题节点中进行断言。实际上,在设置节点中进行断言以验证规格设置是否正确是一种常见模式,然后才在主题节点中进行行为断言。在我们这里(诚然有些牵强的)例子中,我们断言实例化的book是否有效Expect(book.IsValid()).To(BeTrue())

使用容器节点组织规格

Ginkgo允许使用容器节点来层次化地组织套件中的规格。Ginkgo提供了三个同义词来创建容器节点:DescribeContextWhen。这三个在功能上是相同的,并且有助于规范叙述流程。通常Describe代码的不同功能,并探索每个功能在不同Context下的行为。

book套件越来越长,并且会从一些层次化的组织中受益。让我们使用容器节点来组织我们到目前为止所拥有的:

var _ = Describe("Books", func() {
  var book *books.Book

  BeforeEach(func() {
    book = &books.Book{
      Title: "Les Miserables",
      Author: "Victor Hugo",
      Pages: 2783,
    }
    Expect(book.IsValid()).To(BeTrue())
  })

  Describe("Extracting the author's first and last name", func() {
    Context("When the author has both names", func() {
      It("can extract the author's last name", func() {
        Expect(book.AuthorLastName()).To(Equal("Hugo"))
      })

      It("can extract the author's first name", func() {
        Expect(book.AuthorFirstName()).To(Equal("Victor"))
      })
    })

    Context("When the author only has one name", func() {
      BeforeEach(func() {
        book.Author = "Hugo"
      })

      It("interprets the single author name as a last name", func() {
        Expect(book.AuthorLastName()).To(Equal("Hugo"))
      })

      It("returns empty for the first name", func() {
        Expect(book.AuthorFirstName()).To(BeZero())
      })
    })

  })
})

使用容器节点有助于澄清套件背后的意图。作者名字规格现在被清晰地分组在一起,我们正在探索我们的代码在不同上下文中的行为。最重要的是,能够在这些上下文中设置额外的设置节点来细化我们的规格设置。

当Ginkgo运行一个规格时,它会运行所有在该规格的层次结构中出现的BeforeEach闭包,从最外层到最内层。如果在同一嵌套级别出现多个BeforeEach节点,它们将按照在测试文件中出现的顺序运行。对于both names规格,Ginkgo将在主题节点闭包之前运行最外层的BeforeEach闭包。对于one names规格,Ginkgo将运行最外层的BeforeEach闭包,然后是设置book.Author = "Hugo"的内层BeforeEach闭包。

以这种方式组织规格也可以帮助我们理解规格覆盖率。我们遗漏了什么额外的上下文?我们应该担心什么边缘情况?让我们添加一些:

var _ = Describe("Books", func() {
  var book *books.Book

  BeforeEach(func() {
    book = &books.Book{
      Title: "Les Miserables",
      Author: "Victor Hugo",
      Pages: 2783,
    }
    Expect(book.IsValid()).To(BeTrue())
  })

  Describe("Extracting the author's first and last name", func() {
    Context("When the author has both names", func() {
      It("can extract the author's last name", func() {
        Expect(book.AuthorLastName()).To(Equal("Hugo"))
      })

      It("can extract the author's first name", func() {
        Expect(book.AuthorFirstName()).To(Equal("Victor"))
      })
    })

    Context("When the author only has one name ", func() {
      BeforeEach(func() {
        book.Author = "Hugo"
      })

      It("interprets the single author name as a last name", func() {
        Expect(book.AuthorLastName()).To(Equal("Hugo"))
      })

      It("returns empty for the first name", func() {
        Expect(book.AuthorFirstName()).To(BeZero())
      })
    })

    Context("When the author has a middle name", func() {
      BeforeEach(func() {
        book.Author = "Victor Marie Hugo"
      })

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

      It("can extract the author's first name", func() {
        Expect(book.AuthorFirstName()).To(Equal("Victor"))
      })
    })

    Context("When the author has no name", func() {
      It("should not be a valid book and returns empty for first and last name", func() {
        book.Author = ""
        Expect(book.IsValid()).To(BeFalse())
        Expect(book.AuthorLastName()).To(BeZero())
        Expect(book.AuthorFirstName()).To(BeZero())
      })
    })
  })
})

这应该涵盖了大多数边缘情况。正如你所见,我们有灵活性来结构化我们的规格。一些开发人员更喜欢在It节点中尽可能地进行单一断言。其他人更喜欢像我们在no name上下文中所做的那样,将多个断言合并到一个It中。两种方法都得到支持并且完全合理。

让我们继续进行,并添加一些额外的行为规格。让我们测试book模型如何处理JSON编码/解码。由于正在描述新的行为,将添加一个新的Describe容器节点:

var _ = Describe("Books", func() {
  var book *books.Book

  BeforeEach(func() {
    book = &books.Book{
      Title: "Les Miserables",
      Author: "Victor Hugo",
      Pages: 2783,
    }
    Expect(book.IsValid()).To(BeTrue())
  })

  Describe("Extracting the author's first and last name", func() { ... })

  Describe("JSON encoding and decoding", func() {
    It("survives the round trip", func() {
      encoded, err := book.AsJSON()
      Expect(err).NotTo(HaveOccurred())

      decoded, err := books.NewBookFromJSON(encoded)
      Expect(err).NotTo(HaveOccurred())

      Expect(decoded).To(Equal(book))
    })

    Describe("some JSON decoding edge cases", func() {
      var err error

      When("the JSON fails to parse", func() {
        BeforeEach(func() {
          book, err = NewBookFromJSON(`{
            "title":"Les Miserables",
            "author":"Victor Hugo",
            "pages":2783oops
          }`)
        })

        It("returns a nil book", func() {
          Expect(book).To(BeNil())
        })

        It("errors", func() {
          Expect(err).To(MatchError(books.ErrInvalidJSON))
        })
      })

      When("the JSON is incomplete", func() {
        BeforeEach(func() {
          book, err = NewBookFromJSON(`{
            "title":"Les Miserables",
            "author":"Victor Hugo"
          }`)
        })

        It("returns a nil book", func() {
          Expect(book).To(BeNil())
        })

        It("errors", func() {
          Expect(err).To(MatchError(books.ErrIncompleteJSON))
        })
      })
    })
  })
})

通过这种方式,我们可以继续扩展我们的套件,同时使用规格树层次结构清晰地划分我们的规格结构。注意,我们在这个例子中使用了When容器变体,因为它读起来很清晰。记住DescribeContextWhen在功能上是等价的别名。

心智模型:Ginkgo如何遍历规格层次结构

我们已经深入探讨了Ginkgo的三种基本节点类型:容器节点、设置节点和主题节点。在我们继续之前,让我们建立一个心智模型,更详细地了解Ginkgo如何遍历和运行规格。

当Ginkgo运行一个套件时,它分为两个阶段树构造阶段后跟运行阶段

在树构造阶段,Ginkgo通过调用它们的闭包来进入所有容器节点,以构造规格树。在这个阶段,Ginkgo捕获并保存它在树中遇到的各种设置和主题节点闭包而不运行它们。只有在这个阶段运行容器节点闭包,Ginkgo不期望在这个阶段遇到任何断言,因为还没有运行规格。

让我们来描绘一下在实践中这是什么样子。考虑以下一组书籍规格:

var _ = Describe("Books", func() {
  var book *books.Book

  BeforeEach(func() {
    //闭包A
    book = &books.Book{
      Title: "Les Miserables",
      Author: "Victor Hugo",
      Pages: 2783,
    }
    Expect(book.IsValid()).To(BeTrue())
  })

  Describe("Extracting names", func() {
    When("author has both names", func() {
      It("extracts the last name", func() {
        //闭包B
        Expect(book.AuthorLastName()).To(Equal("Hugo"))
      })

      It("extracts the first name", func() {
        //闭包C
        Expect(book.AuthorFirstName()).To(Equal("Victor"))
      })
    })

    When("author has one name", func() {
      BeforeEach(func() {
        //闭包D
        book.Author = "Hugo"
      })

      It("extracts the last name", func() {
        //闭包E
        Expect(book.AuthorLastName()).To(Equal("Hugo"))
      })

      It("returns empty first name", func() {
        //闭包F
        Expect(book.AuthorFirstName()).To(BeZero())
      })
    })

  })
})

可以将Ginkgo生成的规格树表示如下:

Describe: "Books"
  |_BeforeEach: <Closure-A>
  |_Describe: "Extracting names"
    |_When: "author has both names"
      |_It: "extracts the last name", <Closure-B>
      |_It: "extracts the first name", <Closure-C>
    |_When: "author has one name"
      |_BeforeEach: <Closure-D>
      |_It: "extracts the last name", <Closure-E>
      |_It: "returns empty first name", <Closure-F>

请注意,Ginkgo只保存设置和主题节点闭包。

一旦规格树构造完成,Ginkgo遍历树以生成一个扁平化的规格列表。对于我们的例子,结果的规格列表看起来像这样:

{
  Texts: ["Books", "Extracting names", "author has both names", "extracts the last name"],
  Closures: <BeforeEach-Closure-A>, <It-Closure-B>
},
{
  Texts: ["Books", "Extracting names", "author has both names", "extracts the first name"],
  Closures: <BeforeEach-Closure-A>, <It-Closure-C>
},
{
  Texts: ["Books", "Extracting names", "author has one name", "extracts the last name"],
  Closures: <BeforeEach-Closure-A>, <BeforeEach-Closure-D>, <It-Closure-E>
},
{
  Texts: ["Books", "Extracting names", "author has one name", "returns empty first name"],
  Closures: <BeforeEach-Closure-A>, <BeforeEach-Closure-D>, <It-Closure-F>
}

如你所见,每个生成的规格恰好有一个主题节点,以及适当的一组设置节点。在运行阶段,Ginkgo按顺序运行规格列表中的每个规格。当运行一个规格时,Ginkgo按正确的顺序调用设置和主题节点闭包,并跟踪任何失败的断言。请注意,容器节点闭包从不在运行阶段被调用。

鉴于这个心智模型,这里有一些常见的陷阱需要避免:

节点只属于容器节点

由于规格树是通过遍历容器节点构造的,所有的Ginkgo节点必须只出现在套件的顶层嵌套在容器节点内。它们不能出现在主题节点或设置节点内。以下内容是无效的:

/* === INVALID === */
var _ = It("has a color", func() {
  Context("when blue", func() { // NO! Nodes can only be nested in containers
    It("is blue", func() { // NO! Nodes can only be nested in containers

    })
  })
})

Ginkgo如果检测到这一点会发出警告。

容器节点中无断言

由于容器节点被调用以构造规格树,但在运行规格时从不运行,断言必须在主题节点或设置节点中。绝不在容器节点中。以下内容是无效的:

/* === INVALID === */
var _ = Describe("book", func() {
  var book *Book
  Expect(book.Title()).To(BeFalse()) // NO! Place in a setup node instead.

  It("tests something", func() {...})
})

Ginkgo如果检测到这一点会发出警告。

避免规格污染:不要在容器节点中初始化变量

我们已经讨论过这一点,但值得重复:“在容器节点中声明,在设置节点中初始化”。由于容器节点只在树构造阶段被调用一次,应该在容器节点中声明闭包变量,但总是在设置节点中初始化它们。以下内容是无效的,可能会导致难以调试的问题:

/* === INVALID === */
var _ = Describe("book", func() {
  book := &books.Book{ // No!
    Title:  "Les Miserables",
    Author: "Victor Hugo",
    Pages:  2783,
  }

  It("is invalid with no author", func() {
    book.Author = "" // bam! we've changed the closure variable and it will never be reset.
    Expect(book.IsValid()).To(BeFalse())
  })

  It("is valid with an author", func() {
    Expect(book.IsValid()).To(BeTrue()) // this will fail if it runs after the previous test
  })
})

应该这样做:

var _ = Describe("book", func() {
  var book *books.Book // declare in container nodes

  BeforeEach(func() {
    book = &books.Book {  //initialize in setup nodes
      Title:  "Les Miserables",
      Author: "Victor Hugo",
      Pages:  2783,
    }
  })

  It("is invalid with no author", func() {
    book.Author = ""
    Expect(book.IsValid()).To(BeFalse())
  })

  It("is valid with an author", func() {
    Expect(book.IsValid()).To(BeTrue())
  })
})

Ginkgo目前没有机制来检测这种失败模式,需要坚持"在容器节点中声明,在设置节点中初始化"以避免规格污染。

分离创建和配置:JustBeforeEach

让我们回到我们不断增长的图书套件,并探索更多的Ginkgo节点。到目前为止,我们已经遇到了BeforeEach设置节点,让我们介绍它的近亲:JustBeforeEach

JustBeforeEach旨在解决一个非常具体的问题,但应该谨慎使用,因为它可能会给测试套件增加复杂性。考虑我们JSON解码书籍测试的以下部分:

Describe("some JSON decoding edge cases", func() {
  var book *books.Book
  var err error

  When("the JSON fails to parse", func() {
    BeforeEach(func() {
      book, err = NewBookFromJSON(`{
        "title":"Les Miserables",
        "author":"Victor Hugo",
        "pages":2783oops
      }`)
    })

    It("returns a nil book", func() {
      Expect(book).To(BeNil())
    })

    It("errors", func() {
      Expect(err).To(MatchError(books.ErrInvalidJSON))
    })
  })

  When("the JSON is incomplete", func() {
    BeforeEach(func() {
      book, err = NewBookFromJSON(`{
        "title":"Les Miserables",
        "author":"Victor Hugo"
      }`)
    })

    It("returns a nil book", func() {
      Expect(book).To(BeNil())
    })

    It("errors", func() {
      Expect(err).To(MatchError(books.ErrIncompleteJSON))
    })
  })
})

在每种情况下,我们从无效的JSON片段创建一个新的book,确保booknil并检查返回了正确的错误。在这里可以进行某种程度的简化。我们可以尝试提取一个共享的BeforeEach,如下所示:

/* === INVALID === */
Describe("some JSON decoding edge cases", func() {
  var book *books.Book
  var err error
  BeforeEach(func() {
    book, err = NewBookFromJSON(???)
    Expect(book).To(BeNil())
  })

  When("the JSON fails to parse", func() {
    It("errors", func() {
      Expect(err).To(MatchError(books.ErrInvalidJSON))
    })
  })

  When("the JSON is incomplete", func() {
    It("errors", func() {
      Expect(err).To(MatchError(books.ErrIncompleteJSON))
    })
  })
})

但是使用BeforeEachIt节点没有办法配置我们用来创建书籍的json,以不同的方式在每个When容器之前调用NewBookFromJSON。这就是JustBeforeEach的用武之地。顾名思义,JustBeforeEach节点在主题节点之前运行但在任何其他BeforeEach节点之后。可以利用这种行为来编写:

Describe("some JSON decoding edge cases", func() {
  var book *books.Book
  var err error
  var json string
    
  JustBeforeEach(func() {
    book, err = NewBookFromJSON(json)
    Expect(book).To(BeNil())
  })

  When("the JSON fails to parse", func() {
    BeforeEach(func() {
      json = `{
        "title":"Les Miserables",
        "author":"Victor Hugo",
        "pages":2783oops
      }`
    })

    It("errors", func() {
      Expect(err).To(MatchError(books.ErrInvalidJSON))
    })
  })

  When("the JSON is incomplete", func() {
    BeforeEach(func() {
      json = `{
        "title":"Les Miserables",
        "author":"Victor Hugo"
      }`
    })

    It("errors", func() {
      Expect(err).To(MatchError(books.ErrIncompleteJSON))
    })
  })
})

当Ginkgo运行这些规格时,它将首先运行BeforeEach设置闭包,从而填充json变量,然后然后运行JustBeforeEach设置闭包,从而解码正确的JSON字符串。

抽象地说,JustBeforeEach允许你分离创建配置。创建发生在使用一系列BeforeEach指定和修改的配置的JustBeforeEach中。

BeforeEach一样,可以在不同的容器嵌套级别有多个JustBeforeEach节点。Ginkgo将首先从外到内运行所有的BeforeEach闭包,然后从外到内运行所有的JustBeforeEach闭包。虽然强大和灵活,过度使用JustBeforeEach(特别是嵌套JustBeforeEach!)可能会导致令人困惑的套件,所以要谨慎使用JustBeforeEach

规格清理:AfterEach和DeferCleanup

迄今为止看到的设置节点都在规格的主题闭包之前运行。Ginkgo还提供了在规格的主题闭包之后运行的设置节点:AfterEachJustAfterEach。这些用于在规格之后进行清理,可以在复杂的集成套件中特别有用,其中一些外部系统必须在每个规格之后恢复到原始状态。

这里有一个简单的(如果是做作的!)例子来帮助我们开始。让我们暂时放下怀疑,想象一下我们的“书”模型追踪书籍的重量……并且可以使用环境变量指定用于显示权重的单位。让我们详细说明一下:

Describe("Reporting book weight", func() {
  var book *books.Book

  BeforeEach(func() {
    book = &books.Book{
      Title: "Les Miserables",
      Author: "Victor Hugo",
      Pages: 2783,
      Weight: 500,
    }
  })

  Context("with no WEIGHT_UNITS environment set", func() {
    BeforeEach(func() {
      err := os.Unsetenv("WEIGHT_UNITS")
      Expect(err).NotTo(HaveOccurred())
    })

    It("reports the weight in grams", func() {
      Expect(book.HumanReadableWeight()).To(Equal("500g"))
    })
  })

  Context("when WEIGHT_UNITS is set to oz", func() {
    BeforeEach(func() {
      err := os.Setenv("WEIGHT_UNITS", "oz")
      Expect(err).NotTo(HaveOccurred())
    })

    It("reports the weight in ounces", func() {
      Expect(book.HumanReadableWeight()).To(Equal("17.6oz"))
    })
  })

  Context("when WEIGHT_UNITS is invalid", func() {
    BeforeEach(func() {
      err := os.Setenv("WEIGHT_UNITS", "smoots")
      Expect(err).NotTo(HaveOccurred())
    })

    It("errors", func() {
      weight, err := book.HumanReadableWeight()
      Expect(weight).To(BeZero())
      Expect(err).To(HaveOccurred())
    })
  })
})

这些规格是…OK。但我们还有一个微妙的问题:我们没有清理我们覆盖的WEIGHT_UNITS值。这是规格污染的一个例子,可能会导致不相关的规格出现微妙的失败。

让我们使用AfterEach来解决这个问题:

Describe("Reporting book weight", func() {
  var book *books.Book

  BeforeEach(func() {
    book = &books.Book{
      Title: "Les Miserables",
      Author: "Victor Hugo",
      Pages: 2783,
      Weight: 500,
    }
  })

  AfterEach(func() {
    err := os.Unsetenv("WEIGHT_UNITS")
    Expect(err).NotTo(HaveOccurred())
  })

  Context("with no WEIGHT_UNITS environment set", func() {
    BeforeEach(func() {
      err := os.Unsetenv("WEIGHT_UNITS")
      Expect(err).NotTo(HaveOccurred())
    })

    It("reports the weight in grams", func() {
      Expect(book.HumanReadableWeight()).To(Equal("500g"))
    })
  })

  Context("when WEIGHT_UNITS is set to oz", func() {
    BeforeEach(func() {
      err := os.Setenv("WEIGHT_UNITS", "oz")
      Expect(err).NotTo(HaveOccurred())
    })

    It("reports the weight in ounces", func() {
      Expect(book.HumanReadableWeight()).To(Equal("17.6oz"))
    })
  })

  Context("when WEIGHT_UNITS is invalid", func() {
    BeforeEach(func() {
      err := os.Setenv("WEIGHT_UNITS", "smoots")
      Expect(err).NotTo(HaveOccurred())
    })

    It("errors", func() {
      weight, err := book.HumanReadableWeight()
      Expect(weight).To(BeZero())
      Expect(err).To(HaveOccurred())
    })
  })
})

现在我们保证在每个规格之后清除WEIGHT_UNITS,因为Ginkgo会在每个规格的主题节点之后运行AfterEach节点的闭包……

…但我们仍然有一个微妙的问题。通过在AfterEach中清除它,我们假设WEIGHT_UNITS在规格运行时没有被设置。但也许它被设置了?我们真正想要做的是恢复WEIGHT_UNITS到它的原始值。我们可以通过首先记录原始值来解决这个问题:

Describe("Reporting book weight", func() {
  var book *books.Book
  var originalWeightUnits string

  BeforeEach(func() {
    book = &books.Book{
      Title: "Les Miserables",
      Author: "Victor Hugo",
      Pages: 2783,
      Weight: 500,
    }
    originalWeightUnits = os.Getenv("WEIGHT_UNITS")
  })

  AfterEach(func() {
    err := os.Setenv("WEIGHT_UNITS", originalWeightUnits)
    Expect(err).NotTo(HaveOccurred())
  })
  ...
})

这样,规格现在将正确地清理自己。

在这一点上,一个快速的说明,你可能会注意到book.HumanReadableWeight()返回了两个值——一个weight字符串和一个error。这是一种常见的模式,Gomega对它有一流的支持。断言:

Expect(book.HumanReadableWeight()).To(Equal("17.6oz"))

实际上在底层进行了两个断言。即book.HumanReadableWeight返回的第一个值等于"17.6oz",并且任何后续值(即返回的error)是nil。这种优雅的内联错误处理可以使你的规格更加易读。

清理代码:DeferCleanup

像上面这样的设置和清理模式在Ginkgo套件中很常见。然而,AfterEach节点往往会将清理代码与设置代码分开。我们不得不创建一个originalWeightUnits闭包变量来在BeforeEach中跟踪原始环境变量,并将其传递给AfterEach——这感觉很嘈杂,容易出错。

Ginkgo提供了DeferCleanup()函数来帮助解决这种用例,并将规格设置更接近规格清理。这是我们的例子使用DeferCleanup()的样子:

Describe("Reporting book weight", func() {
  var book *books.Book

  BeforeEach(func() {
    ...
    originalWeightUnits := os.Getenv("WEIGHT_UNITS")
    DeferCleanup(func() {
      err := os.Setenv("WEIGHT_UNITS", originalWeightUnits)
      Expect(err).NotTo(HaveOccurred())
    })
  })
  ...
})

正如你所见,DeferCleanup()可以将我们的意图清理更接近我们的设置代码,并避免提取一个单独的闭包变量。乍一看,这个代码可能看起来令人困惑——正如我们上面讨论的,Ginkgo不允许你在设置或主题节点内定义节点。然而,DeferCleanup不是一个Ginkgo节点,而是一个便利函数,它知道如何跟踪清理代码并在规格生命周期的正确时间运行。

在底层,DeferCleanup正在生成一个动态的AfterEach节点,并将其添加到运行中的规格。这个细节并不重要——你可以简单地假设DeferCleanup中的代码具有与AfterEach中代码相同的运行时语义。

DeferCleanup还有一些额外的技巧。

如上所示,DeferCleanup可以传递一个不接受参数且不返回值的函数。你也可以传递一个返回值的函数。DeferCleanup会忽略所有这些返回值,除了最后一个。如果最后一个返回值是非nil错误——这是一个常见的Go模式——DeferCleanup会使规格失败。这允许我们将我们的例子重写为:

Describe("Reporting book weight", func() {
  var book *books.Book

  BeforeEach(func() {
    ...
    originalWeightUnits := os.Getenv("WEIGHT_UNITS")
    DeferCleanup(func() error {
      return os.Setenv("WEIGHT_UNITS", originalWeightUnits)
    })
  })
  ...
})

也可以传递一个接受参数的函数,然后将这些参数直接传递给DeferCleanup。这些参数将被捕获并在清理被触发时传递给函数。这允许我们将我们的例子再次重写为:

Describe("Reporting book weight", func() {
  var book *books.Book

  BeforeEach(func() {
    ...
    DeferCleanup(os.Setenv, "WEIGHT_UNITS", os.Getenv("WEIGHT_UNITS"))
  })
  ...
})

在这里,DeferCleanup捕获了WEIGHT_UNITS的原始值,如os.Getenv("WEIGHT_UNITS")返回的,然后在清理被触发后将两者都传递给os.Setenv,并断言os.Setenv返回的错误是nil。我们已经将清理代码减少到了一行!

分离诊断收集和拆除:JustAfterEach

我们还没有讨论,但Ginkgo也提供了一个JustAfterEach设置节点。JustAfterEach闭包在主题节点之后运行但在任何AfterEach闭包之前。如果你需要在调用AfterEach中的清理代码之前收集有关规格的诊断信息,这可能会很有用。这里有一个快速的例子:

Describe("Saving books to a database", func() {
  AfterEach(func() {
    dbClient.Clear() //clear out the database between tests
  })

  JustAfterEach(func() {
    if CurrentSpecReport().Failed() {
      AddReportEntry("db-dump", dbClient.Dump())
    }
  })

  It("saves the book", func() {
    err := dbClient.Save(book)
    Expect(err).NotTo(HaveOccurred())
  })
})

我们在这里引入了一些新概念,我们将在后面更详细地讨论。这个容器中的JustAfterEach闭包总是在主题闭包之后运行,但在AfterEach闭包之前。当它运行时,它会检查当前规格是否失败(CurrentSpecReport().Failed()),如果检测到失败,它会下载数据库的转储dbClient.Dump()并将其附加到规格报告AddReportEntry()。重要的是,这必须在AfterEach中的dbClient.Clear()调用之前运行,所以我们使用了一个JustAfterEach。当然,我们也可以将这种诊断行为内联到我们的AfterEach中。

JustBeforeEach一样,JustAfterEach也可以嵌套在多个容器中。这样做可能会有强大的结果,但可能会导致令人困惑的测试套件——所以要谨慎使用嵌套的JustAfterEach

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值