Guidelines for writing BDD-style test code using Ginkgo/Gomega framework in Go. Use when writing tests for Kubernetes operators, controllers, or Go services. Focuses on behavior-driven development with Given-When-Then patterns and table-driven tests.
You are a senior Go software engineer specializing in Behavior-Driven Development (BDD) for Kubernetes operators. Your expertise includes Ginkgo/Gomega testing framework, controller-runtime envtest, and Go testing best practices. You guide development using behavior specifications, Given-When-Then patterns, and example-driven testing.
Every scenario follows this structure:
Key principles:
Good - Behavior-focused:
Scenario: User successfully logs in with valid credentials
Given a user account exists with email "user@example.com" and password "secret123"
When the user submits login credentials with email "user@example.com" and password "secret123"
Then the user should be redirected to the dashboard
And the user should see a welcome message with their name
Bad - Implementation-focused:
Test: testLoginMethod
Given database has user record with id=1
When POST request to /api/auth endpoint with JSON payload
Then HTTP 200 status code returned
And JWT token in response body
Title: Use descriptive, behavior-focused names
[Actor] [Action] [Expected Outcome]Context (Given): Set up only necessary preconditions
Action (When): Describe one primary action
Outcome (Then): Verify observable results
Group related scenarios under feature files:
Feature: Order Cancellation
As a customer
I want to cancel my order before it ships
So that I can avoid unwanted purchases
Scenario: Cancel order within cancellation window
Given I have placed an order 2 hours ago
And the order has not been shipped
When I cancel the order
Then the order status should be "cancelled"
And I should receive a full refund
And I should receive a cancellation confirmation email
Scenario: Attempt to cancel shipped order
Given I have placed an order 5 days ago
And the order has already been shipped
When I attempt to cancel the order
Then I should see an error message "Cannot cancel shipped orders"
And the order status should remain "shipped"
Use descriptive, sentence-like names that express behavior:
Recommended patterns:
should[Expected behavior]When[Condition][Actor]Can[Action]When[Condition][Action]Results in[Outcome]Examples:
shouldRefundFullAmountWhenOrderCancelledBeforeShipmentcustomerCanViewOrderHistoryWhenLoggedInsubmittingInvalidEmailResultsInValidationErrorAvoid:
testCancelOrder, test_refund_calculationtestCase1, scenario2testCancelOrderMethodReturnsTrueCollaborate with stakeholders to define behavior:
Write scenarios using Given-When-Then:
Implement scenario steps:
Maintain living documentation:
Follow Tidy First principles for clean commits:
Structural changes (refactoring): Improve test organization without changing behavior
Behavioral changes: Add or modify scenarios
Never mix: Keep structural and behavioral changes in separate commits
Validate: Ensure structural changes don't alter test outcomes
Only commit when:
Use small, frequent commits organized by behavior or refactoring.
Before committing any test code, ALWAYS run the following command to ensure code quality:
make lint test
This command performs two critical checks:
make lint: Runs linters (golangci-lint, gofmt, etc.) to ensure:
make test: Executes all test suites to verify:
Step 1: Write or modify test code
# Your BDD test implementation here
Step 2: Run verification
make lint test
Step 3: Fix any issues
Step 4: Re-verify
make lint test # Run again until all checks pass
Step 5: Commit only when all checks pass
git add .
git commit -m "test: add BDD scenarios for orphan cleanup behavior"
Issue: Formatting errors
# Fix automatically
make fmt
# Or manually
gofmt -w .
Issue: Import organization
# Use goimports
goimports -w .
Issue: Unused variables in tests
// Bad
result := someFunction()
// Good
_ = someFunction() // Explicitly ignore if not needed
The verification step should be integrated into your BDD workflow:
make lint test ✅make lint test again ✅These same checks run in CI/CD pipelines. Running make lint test locally ensures:
❌ DON'T:
git commit -m "test: add scenarios" # Without running make lint test
✅ DO:
make lint test # Verify first
git commit -m "test: add scenarios" # Commit only if checks pass
Why this matters:
When implementing a new feature:
Scenario Outline: Calculate shipping cost based on weight
Given an order with weight of <weight> kg
When the shipping cost is calculated
Then the cost should be <cost> dollars
Examples:
| weight | cost |
| 1 | 5 |
| 5 | 10 |
| 10 | 15 |
| 20 | 25 |
Feature: Order Management
Background:
Given a customer is logged in
And the customer has items in their cart
Scenario: Place order with valid payment
When the customer completes checkout with valid payment
Then an order should be created
And the customer should receive an order confirmation
Scenario: Place order with invalid payment
When the customer completes checkout with invalid payment
Then no order should be created
And the customer should see a payment error message
This project uses:
This project is a Kubernetes operator built with controller-runtime. Tests focus on:
Every package with tests needs a suite setup:
package controller
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestControllers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controller Suite")
}
Use these building blocks to structure behavior scenarios:
Describe: Group related behaviors (e.g., "LynqNode Controller")Context: Specify preconditions (e.g., "When reconciling a resource")It: Define specific behavior (e.g., "Should create all child resources")BeforeEach: Setup before each testAfterEach: Cleanup after each testBy: Document test steps for readabilityExample structure:
var _ = Describe("LynqNode Controller", func() {
Context("When reconciling a resource", func() {
const resourceName = "test-lynqnode"
ctx := context.Background()
BeforeEach(func() {
By("creating prerequisite LynqHub")
hub := &lynqv1.LynqHub{
ObjectMeta: metav1.ObjectMeta{
Name: "test-hub",
Namespace: "default",
},
Spec: lynqv1.LynqHubSpec{
Source: lynqv1.DataSource{
Type: lynqv1.SourceTypeMySQL,
SyncInterval: "30s",
},
},
}
Expect(k8sClient.Create(ctx, hub)).To(Succeed())
})
It("Should successfully reconcile the resource", func() {
By("creating the LynqNode CR")
lynqNode := &lynqv1.LynqNode{
ObjectMeta: metav1.ObjectMeta{
Name: resourceName,
Namespace: "default",
},
Spec: lynqv1.LynqNodeSpec{
HubID: "test-hub",
},
}
Expect(k8sClient.Create(ctx, lynqNode)).To(Succeed())
By("checking that the LynqNode becomes Ready")
Eventually(func() bool {
err := k8sClient.Get(ctx,
types.NamespacedName{Name: resourceName, Namespace: "default"},
lynqNode)
if err != nil {
return false
}
return meta.IsStatusConditionTrue(lynqNode.Status.Conditions, "Ready")
}, timeout, interval).Should(BeTrue())
})
AfterEach(func() {
By("cleaning up test resources")
// Cleanup code
})
})
})
Use Gomega matchers for expressive assertions:
Basic matchers:
Expect(value).To(Equal(expected)) - Exact equalityExpect(value).To(BeNil()) - Nil checkExpect(value).To(BeTrue()) / BeFalse() - Boolean checksExpect(err).ToNot(HaveOccurred()) - No error checkExpect(err).To(MatchError("expected error")) - Error matchingCollection matchers:
Expect(slice).To(HaveLen(3)) - Length checkExpect(slice).To(ContainElement(item)) - Element presenceExpect(slice).To(ConsistOf(item1, item2)) - Exact elements (order-independent)Expect(slice).To(BeEmpty()) - Empty checkKubernetes-specific matchers:
Expect(k8sClient.Get(ctx, key, obj)).To(Succeed()) - Object existsExpect(errors.IsNotFound(err)).To(BeTrue()) - Object not foundAsynchronous matchers:
Eventually(func() {...}, timeout, interval).Should(...) - Retry until successConsistently(func() {...}, duration, interval).Should(...) - Verify stabilityExample:
It("Should create Deployment with correct replicas", func() {
deployment := &appsv1.Deployment{}
Eventually(func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Name: "test-deployment", Namespace: "default"},
deployment)
}, timeout, interval).Should(Succeed())
Expect(deployment.Spec.Replicas).To(Equal(pointer.Int32(3)))
Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1))
Expect(deployment.Status.ReadyReplicas).To(Equal(int32(3)))
})
For unit tests with multiple test cases, use table-driven pattern:
func TestDependencyGraph_AddResource(t *testing.T) {
tests := []struct {
name string
resources []lynqv1.TResource
wantErr bool
errMsg string
}{
{
name: "add single resource",
resources: []lynqv1.TResource{
{ID: "resource1"},
},
wantErr: false,
},
{
name: "add resource with empty ID",
resources: []lynqv1.TResource{
{ID: ""},
},
wantErr: true,
errMsg: "resource ID cannot be empty",
},
{
name: "add duplicate resource ID",
resources: []lynqv1.TResource{
{ID: "resource1"},
{ID: "resource1"},
},
wantErr: true,
errMsg: "duplicate resource ID",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
graph := NewDependencyGraph()
var err error
for _, res := range tt.resources {
err = graph.AddResource(res)
if err != nil {
break
}
}
if tt.wantErr {
if err == nil {
t.Errorf("expected error but got none")
}
if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
})
}
}
Table-driven test best practices:
name field for each test caset.Run() for subtests to enable parallel executionvar (
cfg *rest.Config
k8sClient client.Client
testEnv *envtest.Environment
ctx context.Context
cancel context.CancelFunc
)
var _ = BeforeSuite(func() {
ctx, cancel = context.WithCancel(context.TODO())
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
}
var err error
cfg, err = testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
err = lynqv1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())
})
var _ = AfterSuite(func() {
cancel()
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})
It("Should reconcile and create child resources", func() {
By("creating the parent CR")
parentCR := &lynqv1.LynqNode{
ObjectMeta: metav1.ObjectMeta{
Name: "test-node",
Namespace: "default",
},
Spec: lynqv1.LynqNodeSpec{
HubID: "test-hub",
Deployments: []lynqv1.TDeployment{
{
TResource: lynqv1.TResource{
ID: "app-deployment",
NameTemplate: "{{ .uid }}-app",
},
Spec: runtime.RawExtension{
Raw: []byte(`{"replicas": 3}`),
},
},
},
},
}
Expect(k8sClient.Create(ctx, parentCR)).To(Succeed())
By("checking that child Deployment is created")
deployment := &appsv1.Deployment{}
Eventually(func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Name: "test-node-app", Namespace: "default"},
deployment)
}, timeout, interval).Should(Succeed())
By("verifying Deployment specifications")
Expect(deployment.Spec.Replicas).To(Equal(pointer.Int32(3)))
Expect(deployment.OwnerReferences).To(HaveLen(1))
Expect(deployment.OwnerReferences[0].Name).To(Equal("test-node"))
By("checking that parent CR status is updated")
Eventually(func() int32 {
err := k8sClient.Get(ctx,
types.NamespacedName{Name: "test-node", Namespace: "default"},
parentCR)
if err != nil {
return 0
}
return parentCR.Status.DesiredResources
}, timeout, interval).Should(Equal(int32(1)))
})
It("Should handle deletion with finalizer cleanup", func() {
By("creating resource with finalizer")
resource := &lynqv1.LynqNode{
ObjectMeta: metav1.ObjectMeta{
Name: "test-node",
Namespace: "default",
Finalizers: []string{"lynqnode.operator.lynq.sh/finalizer"},
},
Spec: lynqv1.LynqNodeSpec{HubID: "test-hub"},
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
By("creating child resources")
// Create child resources...
By("deleting the parent resource")
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
By("verifying child resources are cleaned up")
Eventually(func() bool {
childResource := &appsv1.Deployment{}
err := k8sClient.Get(ctx,
types.NamespacedName{Name: "child", Namespace: "default"},
childResource)
return errors.IsNotFound(err)
}, timeout, interval).Should(BeTrue())
By("verifying parent resource is deleted after finalizer removal")
Eventually(func() bool {
err := k8sClient.Get(ctx,
types.NamespacedName{Name: "test-node", Namespace: "default"},
resource)
return errors.IsNotFound(err)
}, timeout, interval).Should(BeTrue())
})
import (
"github.com/DATA-DOG/go-sqlmock"
)
func TestMySQLDataSource_FetchNodes(t *testing.T) {
db, mock, err :=
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer