Home
cd ../playbooks
Developer ToolsIntermediate

Ginkgo Table and Dynamic Specs

Parameterize and generate Ginkgo test specs correctly — DescribeTable/Entry, the four ways to name an entry, DescribeTableSubtree for multi-It rows, reusable entry sets, and the one gotcha (Entry params evaluate at construction time, not BeforeEach time) that causes most table-spec bugs.

5 minutes
By onsi (Ginkgo)Source
#ginkgo#gomega#golang#testing#table-driven-tests#go-testing

A DescribeTable entry that reads shelf["Les Miserables"] looks correct and compiles fine — but it reads a nil map, because Ginkgo evaluates every Entry argument when the test tree is built, before a single BeforeEach has run, and that one timing fact breaks more table-driven Go tests than any other Ginkgo quirk.

Who it's for: Go developers writing table-driven tests with Ginkgo and Gomega, teams migrating from repetitive It blocks to DescribeTable, anyone debugging a table spec where an Entry sees a nil or zero-value variable, engineers extracting shared It blocks across Contexts into reusable closures

Example

"Why does my DescribeTable entry get a nil pointer even though BeforeEach sets it up?" → The construction-time-vs-run-time explanation with a wrong/right code pair, plus the struct-per-row pattern for tables with many parameters and DescribeTableSubtree for entries that need multiple It blocks each

CLAUDE.md Template

New here? 3-minute setup guide → | Already set up? Copy the template below.

# Ginkgo Table and Dynamic Specs

Parameterize and generate Go test specs with Ginkgo's `DescribeTable`/`Entry` DSL and the idioms for generating specs from loops and data. All of it is syntactic sugar that runs during the tree-construction phase, before any spec actually runs — the gotchas below all follow from that one fact.

Prefer `DescribeTable`/`DescribeTableSubtree` with shared configuration over multiple repeated `It`s.

## DescribeTable + Entry

`DescribeTable(desc, specFunc, ...Entry)` generates one container holding one `It` per `Entry`. `Entry(desc, params...)`'s params are passed to `specFunc` at run time and must match `specFunc`'s signature — you get a clear runtime message if they don't.

```go
DescribeTable("Extracting the author's first and last name",
	func(author string, isValid bool, firstName, lastName string) {
		book := &books.Book{Title: "My Book", Author: author, Pages: 10}
		Expect(book.IsValid()).To(Equal(isValid))
		Expect(book.AuthorFirstName()).To(Equal(firstName))
		Expect(book.AuthorLastName()).To(Equal(lastName))
	},
	Entry("both names", "Victor Hugo", true, "Victor", "Hugo"),
	Entry("one name", "Hugo", true, "", "Hugo"),
	Entry("no name", "", false, "", ""),
)
```

A `DescribeTable` is just a container, so nest it inside `Describe`/`Context` and surround it with `BeforeEach` — setup runs fresh before each entry's spec.

### The gotcha: Entry params are evaluated at construction time

`Entry(...)` arguments are evaluated when the tree is built — before any `BeforeEach` has run. An `Entry` cannot read a variable initialized in `BeforeEach`; it will see the zero value (a `nil` map or pointer).

```go
var shelf map[string]*books.Book
BeforeEach(func() { shelf = loadShelf() }) // runs at RUN time

// WRONG — shelf is nil when Entry is evaluated at construction time
DescribeTable("category", func(b *books.Book, c books.Category) { ... },
	Entry("novel", shelf["Les Miserables"], books.CategoryNovel), // nil pointer!
)

// RIGHT — pass a key, dereference shelf inside the spec closure (run time)
DescribeTable("category", func(key string, c books.Category) {
	Expect(shelf[key].Category()).To(Equal(c))
},
	Entry("novel", "Les Miserables", books.CategoryNovel),
)
```

## The Four Ways to Describe an Entry

| Mechanism | How |
|---|---|
| Explicit string | `Entry("both names", ...)` |
| `nil` | `Entry(nil, 1, 2, 3)` → auto-named from params: `Entry: 1, 2, 3` |
| Table-level description closure | pass `func(a,b,c int) string {...}` as the 3rd arg to `DescribeTable`; renders every `nil` entry |
| `EntryDescription(fmt)` | `EntryDescription("%d + %d = %d")` as the 3rd arg to `DescribeTable`, or per-entry |

The description closure must return `string` and accept the same params as `specFunc`. Per-entry, the first arg may itself be a closure or `EntryDescription` (overrides the table default):

```go
DescribeTable("addition", func(a, b, c int) { Expect(a + b).To(Equal(c)) },
	EntryDescription("%d + %d = %d"),                              // table default
	Entry(nil, 1, 2, 3),                                          // "1 + 2 = 3"
	Entry("zeros", 0, 0, 0),                                      // explicit
	Entry(EntryDescription("%[3]d = %[1]d + %[2]d"), 10, 100, 110), // per-entry override
	Entry(func(a, b, c int) string { return fmt.Sprintf("%d = %d", a+b, c) }, 4, 3, 7),
)
```

## Decorating Entries

`Entry` and `DescribeTable` accept every Ginkgo decorator: `Entry("flaky case", FlakeAttempts(3), ...)`, `Entry(..., Label("slow"))`. Focus/pending shortcuts: `FEntry`/`PEntry`/`XEntry` (and `FDescribeTable`/`PDescribeTable`). `PEntry` needs no params; focus/pending obey the same precedence as everywhere else in Ginkgo.

## DescribeTableSubtree — Many Its Per Row

When you want a whole subtree (multiple `It`s, their own setup) per entry, use `DescribeTableSubtree`. Its body runs at construction time, once per entry, inside a fresh container — you must place `It`s inside it or no specs are generated:

```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("returns the status code", func() { Expect(resp.StatusCode).To(Equal(code)) })
		It("returns the message", func() {
			body, _ := io.ReadAll(resp.Body)
			Expect(string(body)).To(Equal(message))
		})
	},
	Entry("default", "example.com/response", http.StatusOK, "hello world"),
	Entry("missing", "example.com/missing", http.StatusNotFound, "wat?"),
)
```

## Patterns

- **Struct-per-row** for many params — inscrutable positional entries like `Entry(nil, 12, 1.2, 8.5, 11, 2783)` are unreadable. Define a type and pass it: `Entry(nil, BookFormatting{FontSize: 12, LineHeight: 1.2, ...}, 2783)`.
- **Reusable `[]TableEntry`** — share one entry set across tables: `var InvalidBooks = []TableEntry{ Entry("empty", &books.Book{}), ... }`, then `DescribeTable("storing errors", storeFn, InvalidBooks)` and `DescribeTable("reading errors", readFn, InvalidBooks)`. Or feed the slice to `DescribeTableSubtree` to attach multiple specs per entry.

## Loading Fixture Data: Do It in TestXxx, Not BeforeSuite

If the spec structure depends on external data, that data must be available *during* tree construction. `BeforeSuite` runs in the run phase — too late; a loop reading a `BeforeSuite`-populated slice generates zero specs. Load it in the `TestXxx` bootstrap function before `RunSpecs`:

```go
var fixtureBooks []*books.Book

func TestBooks(t *testing.T) {
	RegisterFailHandler(Fail)
	g := NewGomegaWithT(t) // wrap t to assert before RunSpecs
	fixtureBooks = LoadFixturesFrom("./fixtures/books.json")
	g.Expect(fixtureBooks).NotTo(BeEmpty())
	RunSpecs(t, "Books Suite")
}

var _ = Describe("fixtures", func() {
	for _, book := range fixtureBooks { // populated before construction — works
		book := book
		It("stores "+book.Title, func() { Expect(library.Store(book)).To(Succeed()) })
	}
})
```

This works because `TestBooks` runs before tree construction, so `fixtureBooks` is populated when the loop runs, and because the function passed to `Describe` is not invoked until tree construction time.

## Shared Behaviors

To reuse identical `It`s across `Context`s that differ only in setup, put the `It`s in a closure and call it inside each `Context` body — it runs at construction time, adding those specs to each context. Because the closure is defined in the same scope, it closes over the shared variable that each `Context`'s `BeforeEach` configures:

```go
AssertFailedBehavior := func() {
	It("can't be stored", func() { Expect(library.IsStorable(book)).To(BeFalse()) })
	It("fails to store", func() { Expect(library.Store(book)).To(MatchError(books.ErrStoringBook)) })
}
Context("when the book has no title", func() {
	BeforeEach(func() { book = &books.Book{Author: "Victor Hugo", Pages: 2783} })
	AssertFailedBehavior()
})
Context("when the book is nil", func() {
	BeforeEach(func() { book = nil })
	AssertFailedBehavior()
})
```

Get new playbooks like this one

One email a week with new Claude Code workflows. Free, like everything here.

No spam. Unsubscribe anytime.

README.md

What This Does

A focused reference for Ginkgo's table-driven and dynamically generated spec idioms, anchored on the one fact that explains every gotcha: DescribeTable, Entry, and loop-generated specs are all syntactic sugar that runs during Ginkgo's tree-construction phase, before any BeforeEach or spec body executes. Covers DescribeTable/Entry syntax, the four ways to name or auto-name an entry, decorating entries with the same decorators as any other node, DescribeTableSubtree for entries that need multiple Its and their own setup, the struct-per-row pattern for tables with many parameters, reusable []TableEntry slices shared across tables, and the correct place to load fixture data that determines spec structure (TestXxx before RunSpecs, never BeforeSuite).

The centerpiece is the construction-time-vs-run-time distinction: an Entry's arguments are evaluated when the tree is built, so an entry that tries to read a BeforeEach-initialized variable silently gets the zero value — a nil map or pointer — instead of an error. The fix (pass a key, dereference inside the spec closure) and the same principle applied to shared-behavior closures across Contexts round out the reference.


Quick Start

Step 1: Create a Project Folder

mkdir ginkgo-tables && cd ginkgo-tables

Step 2: Download the Template

Click Download above, then:

mv ~/Downloads/CLAUDE.md ./

Step 3: Write Table Specs

claude

Point Claude at a Go test file using Ginkgo and ask it to convert repetitive It blocks into a DescribeTable, or to debug a table spec that's seeing unexpected zero values. It will apply the construction-time rule correctly and pick the right entry-naming and row-sharing pattern for the situation.


Tips & Best Practices

  • If an Entry needs data that only exists after setup runs, pass a lookup key instead of the value itself, and resolve it inside the spec closure — the fix is almost always this shape.
  • Switch from positional Entry(nil, 12, 1.2, 8.5, 11, 2783) arguments to a named struct the moment a table grows past two or three parameters; unreadable entries are the most common table-spec code-review complaint.
  • Use DescribeTableSubtree instead of DescribeTable the moment one row needs more than one assertion or its own BeforeEach — trying to cram multiple concerns into a single It per entry is a sign you want the subtree form.

Limitations

  • Covers table and dynamic specs specifically — general Ginkgo authoring (container/subject/setup nodes, JustBeforeEach, GinkgoHelper()) is a separate topic.
  • Assumes familiarity with Ginkgo's two-phase construction/run model; if that model itself is unfamiliar, read Ginkgo's own overview docs first.
  • Code samples target Ginkgo's Go DSL — the concepts (deferred evaluation, construction- vs. run-time data) don't transfer directly to other BDD frameworks with different execution models.

$Related Playbooks

Developer Tools

Ginkgo Timeouts and Async Testing

Make Ginkgo specs interruptible and test asynchronous behavior correctly — SpecContext cancellation, NodeTimeout/SpecTimeout/GracePeriod, Eventually/Consistently with proper context propagation, and the two goroutine rules (GinkgoRecover, poll-don't-block) that prevent one failed assertion from crashing an entire Go test suite.

5 minutes
Intermediate
Developer Tools

Writing Ginkgo Specs

Author correct Ginkgo specs — container vs. subject vs. setup nodes, the declare-in-container/initialize-in-setup rule that prevents the most common Ginkgo bug, JustBeforeEach for separating creation from configuration, DeferCleanup for restore-not-clear teardown, and GinkgoHelper() for failure locations that point at the caller instead of the helper.

5 minutes
Beginner
Developer Tools

Git Conflict Resolution Framework

A plan-first framework for resolving Git merge conflicts by combining both branches' intent instead of blindly picking a side — categorized resolution patterns for imports, tests, generated files, configs, code logic, and struct definitions, with a mandatory approval step before any file is touched.

5 minutes
Intermediate
Developer Tools

Frontend UX Pattern Library

Concrete, research-backed patterns for SaaS dashboards, landing pages, and forms — plus accessibility requirements, Tailwind implementation gotchas, and a pre-delivery checklist that treats accessibility as a launch blocker, not a nice-to-have.

5 minutes
Intermediate
Developer Tools

Git Commit & PR Automation

Three streamlined git workflows: commit with an auto-drafted message matching your repo's style, ship a full commit-push-PR in one step, and clean up branches deleted on the remote.

5 minutes
Beginner
Developer Tools

AI Crawler Access Audit: Who Can Actually Read Your Site

Maps 14 AI crawlers against your robots.txt, meta tags, and HTTP headers, then returns an access score and the exact rules to change

10 minutes
Intermediate
Developer Tools

llms.txt Generator & Auditor

Generate and validate llms.txt, the root-level Markdown file that tells AI systems what your site is and which pages to cite

10 minutes
Intermediate
Developer Tools

GEO Schema: Structured Data for AI Citation

Audit and generate schema.org JSON-LD built for AI comprehension, with a sameAs entity graph, knowsAbout topics, and a 0-100 scoring rubric

10 minutes
Intermediate
Developer Tools

GEO Technical SEO Audit: 8 Categories, 100 Points

A scored technical audit covering crawlability, indexability, security, Core Web Vitals, and the server-side rendering check that decides whether AI crawlers see your content at all

15 minutes
Advanced
Developer Tools

GEO Toolkit Updater

Pull the latest geo-seo-claude skills, agents, scripts, and schema templates from upstream, with a diff summary before anything is overwritten

5 minutes
Beginner
Developer Tools

Improve: Audit Your Codebase and Write the Plans

Turns Claude into a read-only senior advisor that audits a repo, ranks findings by leverage, and writes self-contained implementation plans to disk for cheaper models to execute

10 minutes
Intermediate
Developer Tools

Full-Output Enforcement: No Placeholders, No Stubs

A short rule set that bans TODO comments, skeleton code, and 'rest follows the same pattern' shortcuts so Claude always delivers complete, runnable output

2 minutes
Beginner

Browse all Developer Tools playbooks →