Skip to content

Commit

Permalink
Introduce DescribeTableSubtree
Browse files Browse the repository at this point in the history
  • Loading branch information
onsi committed Dec 26, 2023
1 parent 4ac3a13 commit 65ec56d
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 23 deletions.
105 changes: 104 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1378,7 +1378,7 @@ DescribeTable("Extracting the author's first and last name",
You'll be notified with a clear message at runtime if the parameter types don't match the spec closure signature.

#### Mental Model: Table Specs are just Syntactic Sugar
`DescribeTable` is simply providing syntactic sugar to convert its Ls into a set of standard Ginkgo nodes. During the [Tree Construction Phase](#mental-model-how-ginkgo-traverses-the-spec-hierarchy) `DescribeTable` is generating a single container node that contains one subject node per table entry. The description for the container node will be the description passed to `DescribeTable` and the descriptions for the subject nodes will be the descriptions passed to the `Entry`s. During the Run Phase, when specs run, each subject node will simply invoke the spec closure passed to `DescribeTable`, passing in the parameters associated with the `Entry`.
`DescribeTable` is simply providing syntactic sugar to convert its entries into a set of standard Ginkgo nodes. During the [Tree Construction Phase](#mental-model-how-ginkgo-traverses-the-spec-hierarchy) `DescribeTable` is generating a single container node that contains one subject node per table entry. The description for the container node will be the description passed to `DescribeTable` and the descriptions for the subject nodes will be the descriptions passed to the `Entry`s. During the Run Phase, when specs run, each subject node will simply invoke the spec closure passed to `DescribeTable`, passing in the parameters associated with the `Entry`.

To put it another way, the table test above is equivalent to:

Expand Down Expand Up @@ -1629,6 +1629,86 @@ var _ = Describe("Math", func() {

Will generate entries named: `1 + 2 = 3`, `-1 + 2 = 1`, `zeros`, `110 = 10 + 100`, and `7 = 7`.

#### Generating Subtree Tables

As we've seen `DescribeTable` takes a function and interprets it as the body of a single `It` function. Sometimes, however, you may want to run a collection of specs for a given table entry. You can do this with `DescribeTableSubtree`:

```go
DescribeTableSubtree("handling requests",
func(url string, code int, message string) {
var resp *http.Response
BeforeEach(func() {
var err error
resp, err = http.Get(url)
Expect(err).NotTo(HaveOccurred())
DeferCleanup(resp.Body.Close)
})

It("should return the expected status code", func() {
Expect(resp.StatusCode).To(Equal(code))
})

It("should return the expected message", func() {
body, err := ioutil.ReadAll(resp.Body)
Expect(err).NotTo(HaveOccurred())
Expect(string(body)).To(Equal(message))
})
},
Entry("default response", "example.com/response", http.StatusOK, "hello world"),
Entry("missing response", "example.com/missing", http.StatusNotFound, "wat?"),
...
)
```

now the body function passed to the table is invoked during the Tree Construction Phase to generate a set of specs for each entry. Each body function is invoked within the context of a new container so that setup nodes will only run for the specs defined in the body function. As with `DescribeTable` this is simply synctactic sugar around Ginkgo's existing DSL. The above example is identical to:

```go

Describe("handling requests", func() {
Describe("default response", func() {
var resp *http.Response
BeforeEach(func() {
var err error
resp, err = http.Get("example.com/response")
Expect(err).NotTo(HaveOccurred())
DeferCleanup(resp.Body.Close)
})

It("should return the expected status code", func() {
Expect(resp.StatusCode).To(Equal(http.StatusOK))
})

It("should return the expected message", func() {
body, err := ioutil.ReadAll(resp.Body)
Expect(err).NotTo(HaveOccurred())
Expect(string(body)).To(Equal("hello world"))
})
})

Describe("missing response", func() {
var resp *http.Response
BeforeEach(func() {
var err error
resp, err = http.Get("example.com/missing")
Expect(err).NotTo(HaveOccurred())
DeferCleanup(resp.Body.Close)
})

It("should return the expected status code", func() {
Expect(resp.StatusCode).To(Equal(http.StatusNotFound))
})

It("should return the expected message", func() {
body, err := ioutil.ReadAll(resp.Body)
Expect(err).NotTo(HaveOccurred())
Expect(string(body)).To(Equal("wat?"))
})
})
})
```

all the infrastructure around generating table entry descriptions applies here as well - though the description will be the title of the generatd container. Note that you **must** add subject nodes in the body function if you want `DescribeHandleSubtree` to add specs.

### Alternatives to Dot-Importing Ginkgo

As shown throughout this documentation, Ginkgo users are encouraged to dot-import the Ginkgo DSL into their test suites to effectively extend the Go language with Ginkgo's expressive building blocks:
Expand Down Expand Up @@ -4127,6 +4207,29 @@ DescribeTable("Reading invalid books always errors", func(book *books.Book) {

```

alternatively you can use `DescribeTableSubtree` to associate multiple specs with a given entry:

```go
DescribeTableSubtree("Handling invalid books", func(book *books.Book) {
Describe("Storing invalid books", func() {
It("always errors", func() {
Expect(library.Store(book)).To(MatchError(books.ErrInvalidBook))
})
})

Describe("Reading invalid books", func() {
It("always errors", func() {
Expect(user.Read(book)).To(MatchError(books.ErrInvalidBook))
})
})
},
Entry("Empty book", &books.Book{}),
Entry("Only title", &books.Book{Title: "Les Miserables"}),
Entry("Only author", &books.Book{Author: "Victor Hugo"}),
Entry("Missing pages", &books.Book{Title: "Les Miserables", Author: "Victor Hugo"})
)
```

### Patterns for Asynchronous Testing

It is common, especially in integration suites, to be testing behaviors that occur asynchronously (either within the same process or, in the case of distributed systems, outside the current test process in some combination of external systems). Ginkgo and Gomega provide the building blocks you need to write effective asynchronous specs efficiently.
Expand Down
9 changes: 7 additions & 2 deletions dsl/table/table_dsl.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
Ginkgo isusually dot-imported via:
Ginkgo is usually dot-imported via:
import . "github.com/onsi/ginkgo/v2"
import . "github.com/onsi/ginkgo/v2"
however some parts of the DSL may conflict with existing symbols in the user's code.
Expand All @@ -23,6 +23,11 @@ var FDescribeTable = ginkgo.FDescribeTable
var PDescribeTable = ginkgo.PDescribeTable
var XDescribeTable = ginkgo.XDescribeTable

var DescribeTableSubtree = ginkgo.DescribeTableSubtree
var FDescribeTableSubtree = ginkgo.FDescribeTableSubtree
var PDescribeTableSubtree = ginkgo.PDescribeTableSubtree
var XDescribeTableSubtree = ginkgo.XDescribeTableSubtree

type TableEntry = ginkgo.TableEntry

var Entry = ginkgo.Entry
Expand Down
47 changes: 47 additions & 0 deletions internal/internal_integration/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,53 @@ var _ = Describe("Table driven tests", func() {
})
})

Describe("constructing subtree tables", func() {
BeforeEach(func() {
success, _ := RunFixture("table subtree happy-path", func() {
DescribeTableSubtree("hello", func(a, b, sum, difference int) {
var actualSum, actualDifference int
BeforeEach(func() {
rt.Run(CurrentSpecReport().ContainerHierarchyTexts[1] + " bef")
actualSum = a + b
actualDifference = a - b
})
It(fmt.Sprintf("%d + %d sums correctly", a, b), func() {
rt.Run(CurrentSpecReport().ContainerHierarchyTexts[1] + " sum")
if actualSum != sum {
F("fail")
}
})
It(fmt.Sprintf("%d - %d subtracts correctly", a, b), func() {
rt.Run(CurrentSpecReport().ContainerHierarchyTexts[1] + " difference")
if actualDifference != difference {
F("fail")
}
})
}, func(a, b, sum, differenct int) string { return fmt.Sprintf("%d,%d", a, b) },
Entry(nil, 1, 1, 2, 0),
Entry(nil, 1, 2, 3, -1),
Entry(nil, 2, 1, 0, 0),
)
})
Ω(success).Should(BeFalse())
})

It("runs all the entries", func() {
Ω(rt).Should(HaveTracked("1,1 bef", "1,1 sum", "1,1 bef", "1,1 difference", "1,2 bef", "1,2 sum", "1,2 bef", "1,2 difference", "2,1 bef", "2,1 sum", "2,1 bef", "2,1 difference"))
})

It("reports on the tests correctly", func() {
Ω(reporter.Did.Names()).Should(Equal([]string{"1 + 1 sums correctly", "1 - 1 subtracts correctly", "1 + 2 sums correctly", "1 - 2 subtracts correctly", "2 + 1 sums correctly", "2 - 1 subtracts correctly"}))
Ω(reporter.Did.Find("1 + 1 sums correctly")).Should(HavePassed())
Ω(reporter.Did.Find("1 - 1 subtracts correctly")).Should(HavePassed())
Ω(reporter.Did.Find("1 + 2 sums correctly")).Should(HavePassed())
Ω(reporter.Did.Find("1 - 2 subtracts correctly")).Should(HavePassed())
Ω(reporter.Did.Find("2 + 1 sums correctly")).Should(HaveFailed("fail", types.NodeTypeIt))
Ω(reporter.Did.Find("2 - 1 subtracts correctly")).Should(HaveFailed("fail", types.NodeTypeIt))
Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(6), NPassed(4), NFailed(2)))
})
})

Describe("Entry Descriptions", func() {
Describe("tables with no table-level entry description functions or strings", func() {
BeforeEach(func() {
Expand Down
113 changes: 93 additions & 20 deletions table_dsl.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ And can explore some Table patterns here: https://onsi.github.io/ginkgo/#table-s
*/
func DescribeTable(description string, args ...interface{}) bool {
GinkgoHelper()
generateTable(description, args...)
generateTable(description, false, args...)
return true
}

Expand All @@ -56,7 +56,7 @@ You can focus a table with `FDescribeTable`. This is equivalent to `FDescribe`.
func FDescribeTable(description string, args ...interface{}) bool {
GinkgoHelper()
args = append(args, internal.Focus)
generateTable(description, args...)
generateTable(description, false, args...)
return true
}

Expand All @@ -66,7 +66,7 @@ You can mark a table as pending with `PDescribeTable`. This is equivalent to `P
func PDescribeTable(description string, args ...interface{}) bool {
GinkgoHelper()
args = append(args, internal.Pending)
generateTable(description, args...)
generateTable(description, false, args...)
return true
}

Expand All @@ -75,6 +75,71 @@ You can mark a table as pending with `XDescribeTable`. This is equivalent to `X
*/
var XDescribeTable = PDescribeTable

/*
DescribeTableSubtree describes a table-driven spec that generates a set of tests for each entry.
For example:
DescribeTableSubtree("a subtree table",
func(url string, code int, message string) {
var resp *http.Response
BeforeEach(func() {
var err error
resp, err = http.Get(url)
Expect(err).NotTo(HaveOccurred())
DeferCleanup(resp.Body.Close)
})
It("should return the expected status code", func() {
Expect(resp.StatusCode).To(Equal(code))
})
It("should return the expected message", func() {
body, err := ioutil.ReadAll(resp.Body)
Expect(err).NotTo(HaveOccurred())
Expect(string(body)).To(Equal(message))
})
},
Entry("default response", "example.com/response", http.StatusOK, "hello world"),
Entry("missing response", "example.com/missing", http.StatusNotFound, "wat?"),
)
Note that you **must** place define an It inside the body function.
You can learn more about DescribeTableSubtree here: https://onsi.github.io/ginkgo/#table-specs
And can explore some Table patterns here: https://onsi.github.io/ginkgo/#table-specs-patterns
*/
func DescribeTableSubtree(description string, args ...interface{}) bool {
GinkgoHelper()
generateTable(description, true, args...)
return true
}

/*
You can focus a table with `FDescribeTableSubtree`. This is equivalent to `FDescribe`.
*/
func FDescribeTableSubtree(description string, args ...interface{}) bool {
GinkgoHelper()
args = append(args, internal.Focus)
generateTable(description, true, args...)
return true
}

/*
You can mark a table as pending with `PDescribeTableSubtree`. This is equivalent to `PDescribe`.
*/
func PDescribeTableSubtree(description string, args ...interface{}) bool {
GinkgoHelper()
args = append(args, internal.Pending)
generateTable(description, true, args...)
return true
}

/*
You can mark a table as pending with `XDescribeTableSubtree`. This is equivalent to `XDescribe`.
*/
var XDescribeTableSubtree = PDescribeTableSubtree

/*
TableEntry represents an entry in a table test. You generally use the `Entry` constructor.
*/
Expand Down Expand Up @@ -131,14 +196,14 @@ var XEntry = PEntry
var contextType = reflect.TypeOf(new(context.Context)).Elem()
var specContextType = reflect.TypeOf(new(SpecContext)).Elem()

func generateTable(description string, args ...interface{}) {
func generateTable(description string, isSubtree bool, args ...interface{}) {
GinkgoHelper()
cl := types.NewCodeLocation(0)
containerNodeArgs := []interface{}{cl}

entries := []TableEntry{}
var itBody interface{}
var itBodyType reflect.Type
var internalBody interface{}
var internalBodyType reflect.Type

var tableLevelEntryDescription interface{}
tableLevelEntryDescription = func(args ...interface{}) string {
Expand Down Expand Up @@ -166,11 +231,11 @@ func generateTable(description string, args ...interface{}) {
case t.Kind() == reflect.Func && t.NumOut() == 1 && t.Out(0) == reflect.TypeOf(""):
tableLevelEntryDescription = arg
case t.Kind() == reflect.Func:
if itBody != nil {
if internalBody != nil {
exitIfErr(types.GinkgoErrors.MultipleEntryBodyFunctionsForTable(cl))
}
itBody = arg
itBodyType = reflect.TypeOf(itBody)
internalBody = arg
internalBodyType = reflect.TypeOf(internalBody)
default:
containerNodeArgs = append(containerNodeArgs, arg)
}
Expand Down Expand Up @@ -200,39 +265,47 @@ func generateTable(description string, args ...interface{}) {
err = types.GinkgoErrors.InvalidEntryDescription(entry.codeLocation)
}

itNodeArgs := []interface{}{entry.codeLocation}
itNodeArgs = append(itNodeArgs, entry.decorations...)
internalNodeArgs := []interface{}{entry.codeLocation}
internalNodeArgs = append(internalNodeArgs, entry.decorations...)

hasContext := false
if itBodyType.NumIn() > 0. {
if itBodyType.In(0).Implements(specContextType) {
if internalBodyType.NumIn() > 0. {
if internalBodyType.In(0).Implements(specContextType) {
hasContext = true
} else if itBodyType.In(0).Implements(contextType) && (len(entry.parameters) == 0 || !reflect.TypeOf(entry.parameters[0]).Implements(contextType)) {
} else if internalBodyType.In(0).Implements(contextType) && (len(entry.parameters) == 0 || !reflect.TypeOf(entry.parameters[0]).Implements(contextType)) {
hasContext = true
}
}

if err == nil {
err = validateParameters(itBody, entry.parameters, "Table Body function", entry.codeLocation, hasContext)
err = validateParameters(internalBody, entry.parameters, "Table Body function", entry.codeLocation, hasContext)
}

if hasContext {
itNodeArgs = append(itNodeArgs, func(c SpecContext) {
internalNodeArgs = append(internalNodeArgs, func(c SpecContext) {
if err != nil {
panic(err)
}
invokeFunction(itBody, append([]interface{}{c}, entry.parameters...))
invokeFunction(internalBody, append([]interface{}{c}, entry.parameters...))
})
if isSubtree {
exitIfErr(types.GinkgoErrors.ContextsCannotBeUsedInSubtreeTables(cl))
}
} else {
itNodeArgs = append(itNodeArgs, func() {
internalNodeArgs = append(internalNodeArgs, func() {
if err != nil {
panic(err)
}
invokeFunction(itBody, entry.parameters)
invokeFunction(internalBody, entry.parameters)
})
}

pushNode(internal.NewNode(deprecationTracker, types.NodeTypeIt, description, itNodeArgs...))
internalNodeType := types.NodeTypeIt
if isSubtree {
internalNodeType = types.NodeTypeContainer
}

pushNode(internal.NewNode(deprecationTracker, internalNodeType, description, internalNodeArgs...))
}
})

Expand Down
Loading

0 comments on commit 65ec56d

Please sign in to comment.