Home
cd ../playbooks
Developer ToolsIntermediate

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
By onsi (Ginkgo)Source
#ginkgo#gomega#golang#testing#async-testing#goroutines#go-testing

A goroutine in a Ginkgo spec that calls Expect() without defer GinkgoRecover() doesn't just fail that one test — a panic Ginkgo can't recover crashes the entire suite, and the fix is one line most Go developers writing async tests have never heard of.

Who it's for: Go developers writing Ginkgo/Gomega tests that involve goroutines, channels, or network calls, engineers debugging a Ginkgo spec that hangs instead of failing, teams setting per-node or per-spec timeouts for flaky or slow integration tests, anyone whose Eventually() assertion isn't retrying the way they expect

Example

"My Ginkgo spec hangs forever instead of timing out" → The interruptible-node model explained (only setup/subject nodes accept a cancellable context; propagate ctx into every blocking call), the NodeTimeout vs SpecTimeout vs GracePeriod distinction, and the exact reason a bare <-done channel read blocks past a goroutine's failure instead of surfacing it

CLAUDE.md Template

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

# Ginkgo Timeouts and Async Testing

Make Ginkgo specs interruptible and test asynchronous behavior correctly: `SpecContext`/`context.Context` cancellable nodes, `NodeTimeout`/`SpecTimeout`/`GracePeriod`, the `--timeout` flag, `Abort` and `SIGINT` behavior, Gomega `Eventually`/`Consistently`, and the `defer GinkgoRecover()` rule for goroutines.

Failure in Ginkgo is a panic that Ginkgo recovers; a goroutine that fails needs `defer GinkgoRecover()` or it crashes the whole suite instead of just the one spec.

## Interruptible Nodes: Take a Context, Honor Cancellation

A setup or subject node becomes interruptible simply by accepting a `SpecContext` (or plain `context.Context`) — Ginkgo supplies one automatically:

```go
It("likes to sleep in", func(ctx context.Context) {
  select {
  case <-ctx.Done(): return        // honor cancellation and exit promptly
  case <-time.After(time.Hour): ...
  }
}, NodeTimeout(time.Second))
```

On timeout or interrupt, Ginkgo cancels the context to tell the node to stop. Pass `ctx` down into every blocking call (`libraryClient.SaveBook(ctx, book)`, `exec.CommandContext(ctx, ...)`) so the cancellation actually propagates. Only setup/subject nodes are interruptible — container nodes are not, because they run at construction time.

- `ctx.Deadline()` does NOT report the node's deadline. Ginkgo manages cancellation timing itself (to snapshot a progress report first), so it does not use a `WithDeadline` context. Trust `<-ctx.Done()`, not `Deadline()`.
- `SpecContext` satisfies `context.Context`; you may wrap it (`context.WithValue`) and Ginkgo still cancels the result on time.

## The Timeout Decorators

| Decorator | Scope |
|---|---|
| `NodeTimeout(d)` | Deadline for one interruptible node. |
| `SpecTimeout(d)` | Deadline for the whole spec lifecycle — `It` only. |
| `GracePeriod(d)` | How long Ginkgo waits after cancelling before leaking the node. |

- `SpecTimeout` can only be more lenient than a node's `NodeTimeout` — it caps the sum of all nodes (`BeforeEach`+`It`+`AfterEach`); a per-node `NodeTimeout` inside it is always more stringent.
- Cleanup still runs after a timeout. When `SpecTimeout` fires, Ginkgo cancels the current node, then runs `AfterEach`/`AfterAll`/`DeferCleanup` (each under its own `NodeTimeout`/grace). The timeout is a "mark failed" threshold, not a hard kill.
- A node that ignores cancellation is "leaked," not killed. After the grace period Ginkgo gives up waiting and moves on. Leaking is deliberately preferred over hanging forever — but a leaked goroutine keeps running and can call `Fail`/`AddReportEntry` and pollute a *later* spec. Always make blocking code respond to `ctx.Done()`.
- `DeferCleanup`/`DescribeTable` entries are interruptible too — give the cleanup/entry func a `ctx` first arg; don't capture and reuse the parent node's `ctx` (it's already cancelled by cleanup time).

## Interrupting the Whole Suite

- `ginkgo --timeout=DURATION` — suite-wide budget across all suites (default `1h`).
- `Abort("reason")` — end the suite immediately from within a spec (programmatic interrupt).
- `SIGINT`/`SIGTERM` (`^C`) — interrupt: cancel the current interruptible node, run its cleanup + reporting nodes, skip the rest, exit failed. Escalation: a second interrupt skips cleanup (still runs reporting); a third bails immediately. To inspect a suite without stopping it, send `SIGINFO`/`SIGUSR1` for a progress report.

## Async Assertions: Eventually / Consistently

Use Gomega's `Eventually` (polls until the matcher passes or it times out) and `Consistently` (polls and requires the matcher to hold the whole interval — the way to assert something *doesn't* happen). Three input shapes: bare values (channels, `gbytes`), functions returning `(value[, error])`, and functions taking a `Gomega`.

```go
It("publishes a book", func(ctx SpecContext) {
  buffer := gbytes.NewBuffer()
  c := publisher.Publish(ctx, book, buffer)              // pass ctx so it cancels cleanly
  Eventually(ctx, buffer).Should(gbytes.Say(`Publish complete!`))
  var result publisher.PublishResult
  Eventually(ctx, c).WithTimeout(time.Second).Should(Receive(&result)) // poll, don't <-c
}, SpecTimeout(time.Second*30))
```

Propagate the spec deadline into the poll with `.WithContext(ctx)` or the positional `Eventually(ctx, ...)` — now a node timeout/interrupt makes the `Eventually` exit immediately instead of running its own clock. Gomega also auto-injects the context and `.WithArguments(...)` into a polled function whose first params are `(ctx)`, so `Eventually(client.Connect).WithContext(ctx).Should(Succeed())` works (pass the method reference, not `client.Connect()`).

### Assertions Inside the Polled Function — Use g, Never Global Expect

Pass a function taking `func(g Gomega, ...)` and assert with `g.Expect` so a failed poll retries instead of failing the spec outright:

```go
Eventually(func(g Gomega, ctx SpecContext) {   // g Gomega must be first
  messages, err := gmail.Fetch(ctx, jane.EmailAddress)
  g.Expect(err).NotTo(HaveOccurred())
  g.Expect(messages).To(ContainElement(WithTransform(subjectOf, Equal(want))))
}).WithContext(ctx).Should(Succeed())          // Succeed() = "no failures in the func"
```

Using the global `Expect` inside an `Eventually` defeats the retry — the first failure calls Ginkgo's `Fail` and the spec dies with no second attempt. The local `g` lets `Eventually` catch and re-poll.

## Goroutines: The Two Rules That Bite

```go
It("repaginates", func() {
  done := make(chan any)
  go func() {
    defer GinkgoRecover()                 // RULE 1
    Expect(book.SetFontSize(28)).To(Succeed())
    close(done)
  }()
  Eventually(done).Should(BeClosed())     // RULE 2: poll, don't block on <-done
})
```

1. Any goroutine that may call `Fail` or a Gomega assertion needs `defer GinkgoRecover()`. Ginkgo can't recover a panic raised on a goroutine it didn't start — without this, one failed assertion crashes the entire suite.
2. Don't block the spec on a channel fed by a failing goroutine. If the goroutine fails before `close(done)`, a bare `<-done` blocks until the node *times out* instead of reporting the real failure. `Eventually(done).Should(BeClosed())` lets the failure surface immediately.

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 interruptible-node model and asynchronous testing idioms, built on Ginkgo's actual failure mechanics: a failed assertion is a panic Ginkgo recovers, and a node becomes interruptible simply by accepting a SpecContext or context.Context that Ginkgo cancels on timeout or interrupt. Covers the three timeout decorators (NodeTimeout for one node, SpecTimeout for the whole It lifecycle, GracePeriod for how long Ginkgo waits before giving up on a non-cooperating node), suite-wide interruption via --timeout, Abort, and SIGINT/SIGTERM escalation, and the fact that cleanup still runs after a timeout fires — a timeout is a "mark failed" threshold, not a hard kill.

The async-assertion half covers Gomega's Eventually/Consistently with correct context propagation (.WithContext(ctx) so a node timeout cancels the poll immediately instead of running its own clock) and the critical g.Expect pattern for assertions inside a polled function — using the global Expect there silently defeats retry. It closes with the two goroutine rules that cause the most confusing Ginkgo failures: missing defer GinkgoRecover() crashes the entire suite instead of failing one spec, and blocking on a channel fed by a failing goroutine hangs the node until timeout instead of reporting the real failure.


Quick Start

Step 1: Create a Project Folder

mkdir ginkgo-async && cd ginkgo-async

Step 2: Download the Template

Click Download above, then:

mv ~/Downloads/CLAUDE.md ./

Step 3: Write or Debug Async Specs

claude

Point Claude at a Ginkgo spec that hangs, times out unexpectedly, or crashes the whole suite from one goroutine failure. It will apply the interruptible-node model and the goroutine rules to diagnose and fix it correctly.


Tips & Best Practices

  • Propagate ctx into every blocking call inside an interruptible node — a NodeTimeout only cancels the context; it's the code's job to actually stop when ctx.Done() fires.
  • Always use .WithContext(ctx) (or the positional Eventually(ctx, ...) form) so a spec-level timeout can cut short a stuck poll instead of letting Eventually run out its own independent clock.
  • Any goroutine that touches a Gomega assertion needs defer GinkgoRecover() as its first line — treat this as non-negotiable, since the failure mode without it (an entire suite crash) is far worse than a normal spec failure.

Limitations

  • Covers timeouts and asynchronous testing specifically — general Ginkgo authoring (container/subject/setup nodes, table specs) is a separate topic.
  • ctx.Deadline() does not report a node's actual Ginkgo-managed deadline — code relying on it directly for timing logic will get incorrect values; use <-ctx.Done().
  • Concepts are Ginkgo/Gomega-specific; the underlying Go context.Context cancellation pattern transfers elsewhere, but the decorator names and grace-period mechanics do not.

$Related Playbooks

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
Developer Tools

Image-to-Code: Design Reference to Implementation

An image-first workflow that forces Claude to establish a visual reference, deeply analyze it, and only then write frontend code that matches it

10 minutes
Intermediate

Browse all Developer Tools playbooks →