Run Ginkgo suites in parallel — ginkgo -p / --procs, the separate-process (not goroutine) model, SynchronizedBeforeSuite/SynchronizedAfterSuite vs BeforeSuite, GinkgoParallelProcess() for sharding ports/tmpdirs/databases, building a binary once via gexec, and piping child-process output to GinkgoWriter. Use when parallelizing a suite, speeding up integration tests, fixing parallel-only flakes/races, sharding external resources, or choosing between BeforeSuite and SynchronizedBeforeSuite.
69
84%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Parallelism is where Ginkgo's "every spec is independent" assumption (ginkgo:overview) earns its keep. If your specs are independent, going parallel is one flag. If they aren't, parallelism is how you find out. Docs: https://onsi.github.io/ginkgo/#spec-parallelization, https://onsi.github.io/ginkgo/#patterns-for-parallel-integration-specs.
ginkgo -p # auto-detect optimal process count from CPU cores
ginkgo --procs=4 # pin the count explicitly
ginkgo watch -p # re-run in parallel on every file change-p/--procs require the ginkgo CLI — go test cannot parallelize a Ginkgo suite (it has no server to coordinate processes). Most other features work under go test; parallelism is the headline exception. → ginkgo:running.
This is the one thing to internalize. Ginkgo compiles the suite once (go test -c), then launches N copies of the test binary as separate OS processes. Each process runs the full tree-construction phase independently (so all N build the identical spec list), then pulls specs to run from the CLI, which acts as a server and merges everything into one coherent output stream.
var book don't race across processes — they each get their own book. (Within a process specs still run one at a time.)BeforeSuite runs on every process, so anything it creates is created N times.BeforeSuite runs on every process → N independent resources. Sometimes that's ideal (max isolation). When a resource is expensive or must be shared, use SynchronizedBeforeSuite: set it up once on process #1, then distribute connection info to all processes.
var dbClient *db.Client
var _ = SynchronizedBeforeSuite(func() []byte {
// runs ONLY on process #1; all others wait
dbRunner := db.NewRunner()
Expect(dbRunner.Start()).To(Succeed())
DeferCleanup(dbRunner.Stop) // runs like SynchronizedAfterSuite: after ALL procs finish
return []byte(dbRunner.Address()) // []byte passed to every process
}, func(address []byte) {
// runs on ALL processes with the data from process #1
dbClient = db.NewClient()
Expect(dbClient.Connect(string(address))).To(Succeed())
dbClient.SetNamespace(fmt.Sprintf("namespace-%d", GinkgoParallelProcess()))
})BeforeSuite | SynchronizedBeforeSuite | |
|---|---|---|
| process #1 | runs | first func runs, returns []byte |
| all processes | runs (N independent resources) | second func runs with that []byte |
| use for | per-process resources | one shared resource, info fanned out |
SynchronizedAfterSuite(allProcesses, process1) mirrors it: the first func runs on every process as it finishes; the second runs only on process #1, after all others have exited. The []byte return is optional — a func()/func() form exists too.
GinkgoParallelProcess() returns this process's index (1..N); GinkgoConfiguration() exposes ParallelTotal. Use them to carve up any singleton resource so processes don't collide — the "declare in container, initialize in setup" principle extended to external state.
// ports
addr := fmt.Sprintf("127.0.0.1:%d", 50000+GinkgoParallelProcess())
// tmp dirs
dir := fmt.Sprintf("./tmp-%d", GinkgoParallelProcess())
os.MkdirAll(dir, 0755); DeferCleanup(os.RemoveAll, dir)
// db / kv / multi-tenant namespaces
client.SetNamespace(fmt.Sprintf("test-%d", GinkgoParallelProcess()))Specs that pass in series but fail mysteriously under -p are almost always colliding on a shared singleton (a fixed port, a hard-coded out.epub, one DB key). Shard it by process index. GinkgoT().TempDir() is a zero-config alternative for files (auto-cleaned, but lands in a random location).
BeforeSuite recompiles N times. Compile in SynchronizedBeforeSuite on process #1 via gexec.Build, return the path, and let every process launch its own instance:
var publisherPath string
var _ = SynchronizedBeforeSuite(func() []byte {
path, err := gexec.Build("path/to/publisher")
Expect(err).NotTo(HaveOccurred())
DeferCleanup(gexec.CleanupBuildArtifacts)
return []byte(path)
}, func(path []byte) { publisherPath = string(path) })BeforeEach) is bulletproof but slow; a DB per process spun up in BeforeSuite with snapshot/restore between specs is the common sweet spot; a single shared singleton sharded by GinkgoParallelProcess() namespace works when you can't spin up your own.GinkgoWriter, never os.Stdout/os.Stderr. If a process you spawn outlives the spec and its output is wired to os.Stdout, it hangs Ginkgo's output interceptor. gexec.Start(cmd, GinkgoWriter, GinkgoWriter) is the safe default. → ginkgo:writing-specs (GinkgoWriter).Serial (runs last on process #1). → ginkgo:decorators, ginkgo:ordering-and-flakes.Ordered containers, not definition order. → ginkgo:ordering-and-flakes.Eventually, gexec.Exit) → ginkgo:timeouts-and-async.ginkgo:debugging-failures.f2d0f65
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.