Skip to content

Commit 65ec56d

Browse files
committed
Introduce DescribeTableSubtree
1 parent 4ac3a13 commit 65ec56d

File tree

5 files changed

+260
-23
lines changed

5 files changed

+260
-23
lines changed

docs/index.md

+104-1
Original file line numberDiff line numberDiff line change
@@ -1378,7 +1378,7 @@ DescribeTable("Extracting the author's first and last name",
13781378
You'll be notified with a clear message at runtime if the parameter types don't match the spec closure signature.
13791379

13801380
#### Mental Model: Table Specs are just Syntactic Sugar
1381-
`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`.
1381+
`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`.
13821382

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

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

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

1632+
#### Generating Subtree Tables
1633+
1634+
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`:
1635+
1636+
```go
1637+
DescribeTableSubtree("handling requests",
1638+
func(url string, code int, message string) {
1639+
var resp *http.Response
1640+
BeforeEach(func() {
1641+
var err error
1642+
resp, err = http.Get(url)
1643+
Expect(err).NotTo(HaveOccurred())
1644+
DeferCleanup(resp.Body.Close)
1645+
})
1646+
1647+
It("should return the expected status code", func() {
1648+
Expect(resp.StatusCode).To(Equal(code))
1649+
})
1650+
1651+
It("should return the expected message", func() {
1652+
body, err := ioutil.ReadAll(resp.Body)
1653+
Expect(err).NotTo(HaveOccurred())
1654+
Expect(string(body)).To(Equal(message))
1655+
})
1656+
},
1657+
Entry("default response", "example.com/response", http.StatusOK, "hello world"),
1658+
Entry("missing response", "example.com/missing", http.StatusNotFound, "wat?"),
1659+
...
1660+
)
1661+
```
1662+
1663+
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:
1664+
1665+
```go
1666+
1667+
Describe("handling requests", func() {
1668+
Describe("default response", func() {
1669+
var resp *http.Response
1670+
BeforeEach(func() {
1671+
var err error
1672+
resp, err = http.Get("example.com/response")
1673+
Expect(err).NotTo(HaveOccurred())
1674+
DeferCleanup(resp.Body.Close)
1675+
})
1676+
1677+
It("should return the expected status code", func() {
1678+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1679+
})
1680+
1681+
It("should return the expected message", func() {
1682+
body, err := ioutil.ReadAll(resp.Body)
1683+
Expect(err).NotTo(HaveOccurred())
1684+
Expect(string(body)).To(Equal("hello world"))
1685+
})
1686+
})
1687+
1688+
Describe("missing response", func() {
1689+
var resp *http.Response
1690+
BeforeEach(func() {
1691+
var err error
1692+
resp, err = http.Get("example.com/missing")
1693+
Expect(err).NotTo(HaveOccurred())
1694+
DeferCleanup(resp.Body.Close)
1695+
})
1696+
1697+
It("should return the expected status code", func() {
1698+
Expect(resp.StatusCode).To(Equal(http.StatusNotFound))
1699+
})
1700+
1701+
It("should return the expected message", func() {
1702+
body, err := ioutil.ReadAll(resp.Body)
1703+
Expect(err).NotTo(HaveOccurred())
1704+
Expect(string(body)).To(Equal("wat?"))
1705+
})
1706+
})
1707+
})
1708+
```
1709+
1710+
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.
1711+
16321712
### Alternatives to Dot-Importing Ginkgo
16331713

16341714
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:
@@ -4127,6 +4207,29 @@ DescribeTable("Reading invalid books always errors", func(book *books.Book) {
41274207

41284208
```
41294209

4210+
alternatively you can use `DescribeTableSubtree` to associate multiple specs with a given entry:
4211+
4212+
```go
4213+
DescribeTableSubtree("Handling invalid books", func(book *books.Book) {
4214+
Describe("Storing invalid books", func() {
4215+
It("always errors", func() {
4216+
Expect(library.Store(book)).To(MatchError(books.ErrInvalidBook))
4217+
})
4218+
})
4219+
4220+
Describe("Reading invalid books", func() {
4221+
It("always errors", func() {
4222+
Expect(user.Read(book)).To(MatchError(books.ErrInvalidBook))
4223+
})
4224+
})
4225+
},
4226+
Entry("Empty book", &books.Book{}),
4227+
Entry("Only title", &books.Book{Title: "Les Miserables"}),
4228+
Entry("Only author", &books.Book{Author: "Victor Hugo"}),
4229+
Entry("Missing pages", &books.Book{Title: "Les Miserables", Author: "Victor Hugo"})
4230+
)
4231+
```
4232+
41304233
### Patterns for Asynchronous Testing
41314234

41324235
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.

dsl/table/table_dsl.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
2-
Ginkgo isusually dot-imported via:
2+
Ginkgo is usually dot-imported via:
33
4-
import . "github.com/onsi/ginkgo/v2"
4+
import . "github.com/onsi/ginkgo/v2"
55
66
however some parts of the DSL may conflict with existing symbols in the user's code.
77
@@ -23,6 +23,11 @@ var FDescribeTable = ginkgo.FDescribeTable
2323
var PDescribeTable = ginkgo.PDescribeTable
2424
var XDescribeTable = ginkgo.XDescribeTable
2525

26+
var DescribeTableSubtree = ginkgo.DescribeTableSubtree
27+
var FDescribeTableSubtree = ginkgo.FDescribeTableSubtree
28+
var PDescribeTableSubtree = ginkgo.PDescribeTableSubtree
29+
var XDescribeTableSubtree = ginkgo.XDescribeTableSubtree
30+
2631
type TableEntry = ginkgo.TableEntry
2732

2833
var Entry = ginkgo.Entry

internal/internal_integration/table_test.go

+47
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,53 @@ var _ = Describe("Table driven tests", func() {
4444
})
4545
})
4646

47+
Describe("constructing subtree tables", func() {
48+
BeforeEach(func() {
49+
success, _ := RunFixture("table subtree happy-path", func() {
50+
DescribeTableSubtree("hello", func(a, b, sum, difference int) {
51+
var actualSum, actualDifference int
52+
BeforeEach(func() {
53+
rt.Run(CurrentSpecReport().ContainerHierarchyTexts[1] + " bef")
54+
actualSum = a + b
55+
actualDifference = a - b
56+
})
57+
It(fmt.Sprintf("%d + %d sums correctly", a, b), func() {
58+
rt.Run(CurrentSpecReport().ContainerHierarchyTexts[1] + " sum")
59+
if actualSum != sum {
60+
F("fail")
61+
}
62+
})
63+
It(fmt.Sprintf("%d - %d subtracts correctly", a, b), func() {
64+
rt.Run(CurrentSpecReport().ContainerHierarchyTexts[1] + " difference")
65+
if actualDifference != difference {
66+
F("fail")
67+
}
68+
})
69+
}, func(a, b, sum, differenct int) string { return fmt.Sprintf("%d,%d", a, b) },
70+
Entry(nil, 1, 1, 2, 0),
71+
Entry(nil, 1, 2, 3, -1),
72+
Entry(nil, 2, 1, 0, 0),
73+
)
74+
})
75+
Ω(success).Should(BeFalse())
76+
})
77+
78+
It("runs all the entries", func() {
79+
Ω(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"))
80+
})
81+
82+
It("reports on the tests correctly", func() {
83+
Ω(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"}))
84+
Ω(reporter.Did.Find("1 + 1 sums correctly")).Should(HavePassed())
85+
Ω(reporter.Did.Find("1 - 1 subtracts correctly")).Should(HavePassed())
86+
Ω(reporter.Did.Find("1 + 2 sums correctly")).Should(HavePassed())
87+
Ω(reporter.Did.Find("1 - 2 subtracts correctly")).Should(HavePassed())
88+
Ω(reporter.Did.Find("2 + 1 sums correctly")).Should(HaveFailed("fail", types.NodeTypeIt))
89+
Ω(reporter.Did.Find("2 - 1 subtracts correctly")).Should(HaveFailed("fail", types.NodeTypeIt))
90+
Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(6), NPassed(4), NFailed(2)))
91+
})
92+
})
93+
4794
Describe("Entry Descriptions", func() {
4895
Describe("tables with no table-level entry description functions or strings", func() {
4996
BeforeEach(func() {

table_dsl.go

+93-20
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ And can explore some Table patterns here: https://onsi.github.io/ginkgo/#table-s
4646
*/
4747
func DescribeTable(description string, args ...interface{}) bool {
4848
GinkgoHelper()
49-
generateTable(description, args...)
49+
generateTable(description, false, args...)
5050
return true
5151
}
5252

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

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

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

78+
/*
79+
DescribeTableSubtree describes a table-driven spec that generates a set of tests for each entry.
80+
81+
For example:
82+
83+
DescribeTableSubtree("a subtree table",
84+
func(url string, code int, message string) {
85+
var resp *http.Response
86+
BeforeEach(func() {
87+
var err error
88+
resp, err = http.Get(url)
89+
Expect(err).NotTo(HaveOccurred())
90+
DeferCleanup(resp.Body.Close)
91+
})
92+
93+
It("should return the expected status code", func() {
94+
Expect(resp.StatusCode).To(Equal(code))
95+
})
96+
97+
It("should return the expected message", func() {
98+
body, err := ioutil.ReadAll(resp.Body)
99+
Expect(err).NotTo(HaveOccurred())
100+
Expect(string(body)).To(Equal(message))
101+
})
102+
},
103+
Entry("default response", "example.com/response", http.StatusOK, "hello world"),
104+
Entry("missing response", "example.com/missing", http.StatusNotFound, "wat?"),
105+
)
106+
107+
Note that you **must** place define an It inside the body function.
108+
109+
You can learn more about DescribeTableSubtree here: https://onsi.github.io/ginkgo/#table-specs
110+
And can explore some Table patterns here: https://onsi.github.io/ginkgo/#table-specs-patterns
111+
*/
112+
func DescribeTableSubtree(description string, args ...interface{}) bool {
113+
GinkgoHelper()
114+
generateTable(description, true, args...)
115+
return true
116+
}
117+
118+
/*
119+
You can focus a table with `FDescribeTableSubtree`. This is equivalent to `FDescribe`.
120+
*/
121+
func FDescribeTableSubtree(description string, args ...interface{}) bool {
122+
GinkgoHelper()
123+
args = append(args, internal.Focus)
124+
generateTable(description, true, args...)
125+
return true
126+
}
127+
128+
/*
129+
You can mark a table as pending with `PDescribeTableSubtree`. This is equivalent to `PDescribe`.
130+
*/
131+
func PDescribeTableSubtree(description string, args ...interface{}) bool {
132+
GinkgoHelper()
133+
args = append(args, internal.Pending)
134+
generateTable(description, true, args...)
135+
return true
136+
}
137+
138+
/*
139+
You can mark a table as pending with `XDescribeTableSubtree`. This is equivalent to `XDescribe`.
140+
*/
141+
var XDescribeTableSubtree = PDescribeTableSubtree
142+
78143
/*
79144
TableEntry represents an entry in a table test. You generally use the `Entry` constructor.
80145
*/
@@ -131,14 +196,14 @@ var XEntry = PEntry
131196
var contextType = reflect.TypeOf(new(context.Context)).Elem()
132197
var specContextType = reflect.TypeOf(new(SpecContext)).Elem()
133198

134-
func generateTable(description string, args ...interface{}) {
199+
func generateTable(description string, isSubtree bool, args ...interface{}) {
135200
GinkgoHelper()
136201
cl := types.NewCodeLocation(0)
137202
containerNodeArgs := []interface{}{cl}
138203

139204
entries := []TableEntry{}
140-
var itBody interface{}
141-
var itBodyType reflect.Type
205+
var internalBody interface{}
206+
var internalBodyType reflect.Type
142207

143208
var tableLevelEntryDescription interface{}
144209
tableLevelEntryDescription = func(args ...interface{}) string {
@@ -166,11 +231,11 @@ func generateTable(description string, args ...interface{}) {
166231
case t.Kind() == reflect.Func && t.NumOut() == 1 && t.Out(0) == reflect.TypeOf(""):
167232
tableLevelEntryDescription = arg
168233
case t.Kind() == reflect.Func:
169-
if itBody != nil {
234+
if internalBody != nil {
170235
exitIfErr(types.GinkgoErrors.MultipleEntryBodyFunctionsForTable(cl))
171236
}
172-
itBody = arg
173-
itBodyType = reflect.TypeOf(itBody)
237+
internalBody = arg
238+
internalBodyType = reflect.TypeOf(internalBody)
174239
default:
175240
containerNodeArgs = append(containerNodeArgs, arg)
176241
}
@@ -200,39 +265,47 @@ func generateTable(description string, args ...interface{}) {
200265
err = types.GinkgoErrors.InvalidEntryDescription(entry.codeLocation)
201266
}
202267

203-
itNodeArgs := []interface{}{entry.codeLocation}
204-
itNodeArgs = append(itNodeArgs, entry.decorations...)
268+
internalNodeArgs := []interface{}{entry.codeLocation}
269+
internalNodeArgs = append(internalNodeArgs, entry.decorations...)
205270

206271
hasContext := false
207-
if itBodyType.NumIn() > 0. {
208-
if itBodyType.In(0).Implements(specContextType) {
272+
if internalBodyType.NumIn() > 0. {
273+
if internalBodyType.In(0).Implements(specContextType) {
209274
hasContext = true
210-
} else if itBodyType.In(0).Implements(contextType) && (len(entry.parameters) == 0 || !reflect.TypeOf(entry.parameters[0]).Implements(contextType)) {
275+
} else if internalBodyType.In(0).Implements(contextType) && (len(entry.parameters) == 0 || !reflect.TypeOf(entry.parameters[0]).Implements(contextType)) {
211276
hasContext = true
212277
}
213278
}
214279

215280
if err == nil {
216-
err = validateParameters(itBody, entry.parameters, "Table Body function", entry.codeLocation, hasContext)
281+
err = validateParameters(internalBody, entry.parameters, "Table Body function", entry.codeLocation, hasContext)
217282
}
218283

219284
if hasContext {
220-
itNodeArgs = append(itNodeArgs, func(c SpecContext) {
285+
internalNodeArgs = append(internalNodeArgs, func(c SpecContext) {
221286
if err != nil {
222287
panic(err)
223288
}
224-
invokeFunction(itBody, append([]interface{}{c}, entry.parameters...))
289+
invokeFunction(internalBody, append([]interface{}{c}, entry.parameters...))
225290
})
291+
if isSubtree {
292+
exitIfErr(types.GinkgoErrors.ContextsCannotBeUsedInSubtreeTables(cl))
293+
}
226294
} else {
227-
itNodeArgs = append(itNodeArgs, func() {
295+
internalNodeArgs = append(internalNodeArgs, func() {
228296
if err != nil {
229297
panic(err)
230298
}
231-
invokeFunction(itBody, entry.parameters)
299+
invokeFunction(internalBody, entry.parameters)
232300
})
233301
}
234302

235-
pushNode(internal.NewNode(deprecationTracker, types.NodeTypeIt, description, itNodeArgs...))
303+
internalNodeType := types.NodeTypeIt
304+
if isSubtree {
305+
internalNodeType = types.NodeTypeContainer
306+
}
307+
308+
pushNode(internal.NewNode(deprecationTracker, internalNodeType, description, internalNodeArgs...))
236309
}
237310
})
238311

0 commit comments

Comments
 (0)