-
Notifications
You must be signed in to change notification settings - Fork 68
🌱 Migrate e2e tests to Godog BDD framework #2365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
✅ Deploy Preview for olmv1 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR migrates the e2e test suite from traditional Go testing framework to Godog (BDD/Cucumber framework), enabling behavior-driven development with Gherkin feature files. The migration maintains test coverage while reorganizing tests into feature files with step definitions.
Key Changes
- Replaced traditional Go test functions with Godog scenarios and step definitions
- Added Gherkin
.featurefiles describing test behavior in a more readable format - Introduced new test infrastructure (
steps.go,hooks.go) to support BDD testing
Reviewed changes
Copilot reviewed 19 out of 21 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| test/e2e/features_test.go | New test entry point initializing Godog suite with scenario and suite initializers |
| test/e2e/features/steps/steps.go | Implements step definitions mapping Gherkin steps to Go functions |
| test/e2e/features/steps/hooks.go | Provides scenario lifecycle hooks and feature gate detection |
| test/e2e/features/*.feature | Gherkin feature files defining test scenarios (install, update, recover, metrics) |
| test/e2e/features/steps/testdata/*.yaml | YAML templates for test resources (catalogs, RBAC) |
| test/e2e/*_test.go | Removed traditional test files migrated to feature files |
| test/e2e/network_policy_test.go | Added client initialization and helper function |
| go.mod, go.sum | Added Cucumber/Godog dependencies |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
730ba01 to
0482745
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2365 +/- ##
==========================================
+ Coverage 70.61% 74.38% +3.77%
==========================================
Files 93 93
Lines 7333 7333
==========================================
+ Hits 5178 5455 +277
+ Misses 1720 1443 -277
Partials 435 435
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
0482745 to
7038e17
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 20 out of 22 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if stdErr := string(func() *exec.ExitError { | ||
| target := &exec.ExitError{} | ||
| _ = errors.As(err, &target) | ||
| return target | ||
| }().Stderr); !strings.Contains(stdErr, errMsg) { |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential nil pointer dereference when extracting stderr from error. If the error is not of type *exec.ExitError, this will panic. Consider adding a nil check:
waitFor(ctx, func() bool {
_, err := kubectlWithInput(yamlContent, "apply", "-f", "-")
if err == nil {
return false
}
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && strings.Contains(string(exitErr.Stderr), errMsg) {
return true
}
return false
})| if stdErr := string(func() *exec.ExitError { | |
| target := &exec.ExitError{} | |
| _ = errors.As(err, &target) | |
| return target | |
| }().Stderr); !strings.Contains(stdErr, errMsg) { | |
| var exitErr *exec.ExitError | |
| if errors.As(err, &exitErr) { | |
| stdErr := string(exitErr.Stderr) | |
| if !strings.Contains(stdErr, errMsg) { | |
| return false | |
| } | |
| return true | |
| } | |
| return false |
test/e2e/steps/hooks.go
Outdated
| if _, err := kubectl("delete", r.kind, r.name, "-n", sc.namespace); err != nil { | ||
| logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", string(err.(*exec.ExitError).Stderr)) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential nil pointer dereference when asserting error type. If the error is not of type *exec.ExitError, this will panic when accessing .Stderr. Consider checking if the type assertion succeeded:
if _, err := kubectl("delete", r.kind, r.name, "-n", sc.namespace); err != nil {
var exitErr *exec.ExitError
stderr := ""
if errors.As(err, &exitErr) {
stderr = string(exitErr.Stderr)
}
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", stderr)
}| } | ||
|
|
||
| func scenarioCtx(ctx context.Context) *scenarioContext { | ||
| return ctx.Value(scenarioContextKey).(*scenarioContext) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential nil pointer dereference. If ctx.Value(scenarioContextKey) returns nil, this will panic. Consider adding a nil check and returning a helpful error:
func scenarioCtx(ctx context.Context) *scenarioContext {
val := ctx.Value(scenarioContextKey)
if val == nil {
panic("scenario context not found in context")
}
sc, ok := val.(*scenarioContext)
if !ok {
panic("scenario context has wrong type")
}
return sc
}| return ctx.Value(scenarioContextKey).(*scenarioContext) | |
| val := ctx.Value(scenarioContextKey) | |
| if val == nil { | |
| panic("scenario context not found in context") | |
| } | |
| sc, ok := val.(*scenarioContext) | |
| if !ok { | |
| panic("scenario context has wrong type") | |
| } | |
| return sc |
7038e17 to
844eaa1
Compare
844eaa1 to
63e2440
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 20 out of 21 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", string(func() *exec.ExitError { | ||
| target := &exec.ExitError{} | ||
| _ = errors.As(err, &target) | ||
| return target | ||
| }().Stderr)) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential nil pointer dereference. If errors.As(err, &target) returns false, target will be nil and accessing target.Stderr will cause a panic.
Suggested fix:
var stderrStr string
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
stderrStr = string(exitErr.Stderr)
}
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", stderrStr)| logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", string(func() *exec.ExitError { | |
| target := &exec.ExitError{} | |
| _ = errors.As(err, &target) | |
| return target | |
| }().Stderr)) | |
| var stderrStr string | |
| var exitErr *exec.ExitError | |
| if errors.As(err, &exitErr) { | |
| stderrStr = string(exitErr.Stderr) | |
| } | |
| logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", stderrStr) |
joelanford
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a really nice improvement overall. Just some minor comments/questions.
| - "sleep" | ||
| args: | ||
| - "1000" | ||
| image: busybox:1.36 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the same image we've always used?
I recall there being issues in the past with rate limiting from Docker Hub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is the one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we were hitting rate-limiting because we had the image untagged/set to latest, so it would pull every time no matter what. This is tagged so it should be fine.
| """ | ||
| Then ClusterExtension reports Progressing as True with Reason Retrying: | ||
| """ | ||
| error upgrading from currently installed version "1.0.0": no bundles found for package "test" matching version "1.2.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated to this PR, but something we need to fix separately. This message makes it sound like 1.2.0 just doesn't exist. But it does. It just isn't a successor of the currently installed version.
test/e2e/features_test.go
Outdated
| "github.com/spf13/pflag" | ||
| ctrl "sigs.k8s.io/controller-runtime" | ||
| //ctrllog "sigs.k8s.io/controller-runtime/pkg/log" | ||
| "sigs.k8s.io/controller-runtime/pkg/log/zap" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid new zap dependency? I think we're using klog in our main.go's. Can we use that here too?
test/e2e/steps/hooks.go
Outdated
|
|
||
| func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context.Context, error) { | ||
| sc := scenarioCtx(ctx) | ||
| for _, p := range sc.backGroundCmds { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kill and wait processes concurrently? Or does order matter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I think we do not need to wait at all.
| } | ||
| forDeletion = append(forDeletion, sc.addedResources...) | ||
| forDeletion = append(forDeletion, resource{name: sc.namespace, kind: "namespace"}) | ||
| for _, r := range forDeletion { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here: Can we delete objects concurrently?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could try, but not sure what we are gonna gain with it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deletion can sometimes take a little while if finalizers need to be processed. I assume we want foreground deletion so that we can be sure cleanup is complete before we move on. Seems like it could speed up cleanup considerably in those cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, we could do it.
| error for resolved bundle "single-namespace-operator.1.0.0" with version "1.0.0": | ||
| invalid ClusterExtension configuration: invalid configuration: required field "watchNamespace" is missing | ||
| """ | ||
| When ClusterExtension is updated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could strategic merge patch instead and have a smaller yaml to make it more clear what's changing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be an improvement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could strategic merge patch instead and have a smaller yaml to make it more clear what's changing?
We could ofcourse, we craft the grammar and the semantic: how would like to look like? If we would writing user docs, how this should be read by users?
| And resource is applied | ||
| """ | ||
| apiVersion: v1 | ||
| kind: Namespace | ||
| metadata: | ||
| name: single-namespace-operator-target | ||
| """ | ||
| And ClusterExtension is applied |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just nothing the inconsistency here:
- resource is applied
vs - ClusterExtension is applied
The first one is generic, the second one is indicating a very specific resource. What's the reasoning behind this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first one is generic, the second one is indicating a very specific resource. What's the reasoning behind this?
Improved readability/focus on what matters. The fact that we use the same go code under the hood for both step is not important here - the reader should immediately understand what a step is about. Hence, we could even replace the generic step "resource is applied" with something like ([[:alnum:]]+) is applied or even ([[:alnum:]]+) is available so that we better document what is going on. Also, in this particular case, we could even create very concrete step namespace ([[:alnum:]]+) is available that is going to assure that the given namespace is created if not exists already.
| error for resolved bundle "single-namespace-operator.1.0.0" with version "1.0.0": | ||
| invalid ClusterExtension configuration: invalid configuration: required field "watchNamespace" is missing | ||
| """ | ||
| When ClusterExtension is updated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be an improvement.
| forDeletion = append(forDeletion, sc.addedResources...) | ||
| forDeletion = append(forDeletion, resource{name: sc.namespace, kind: "namespace"}) | ||
| for _, r := range forDeletion { | ||
| if _, err := kubectl("delete", r.kind, r.name, "-n", sc.namespace); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no distinction here between namespace-scoped resources (e.g. SAs), and cluster-scoped resources (e.g. CE). 'kubectl' may not complain about the -n argument for cluster-scoped resources, but it's basically wrong.
| result := strings.ReplaceAll(content, "$TEST_NAMESPACE", sc.namespace) | ||
| result = strings.ReplaceAll(result, "$NAME", sc.clusterExtensionName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could see us wanting to expand this list, or even to make this a bit more dynamic. But probably not now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also noting an inconsistency in substitution mechanisms. Here it's $VAR, where as at: https://github.com/operator-framework/operator-controller/pull/2365/files#diff-37528e433a53ab946ef66fda327001b3a125c05c7ac9dfd2b49529fbfdc50cd3R378 it's {var}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also noting an inconsistency in substitution mechanisms. Here it's
$VAR, where as at: https://github.com/operator-framework/operator-controller/pull/2365/files#diff-37528e433a53ab946ef66fda327001b3a125c05c7ac9dfd2b49529fbfdc50cd3R378 it's{var}
IMO, bash-style like variables are understandable/known for a wider audience. We could use those in testdata templates as well.
test/e2e/steps/steps.go
Outdated
| ) | ||
|
|
||
| func kubectl(args ...string) (string, error) { | ||
| cmd := exec.Command("kubectl", args...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we run these tests in other environments, we might need to consider the use of oc as a substitute for kubectl. Here and elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added --k8s.cli command arg that can tweak that.
|
Hi team, what’s the reason for choosing
|
@jianzhangbjz thanks for reaching out. I think I have summarized the motivation in the PR description, let me know if some of the points you raised are unanswered. IMO Ginko for e2e is still less readable for non-devs (and for devs as well) |
Replace traditional Go e2e tests with Godog (Cucumber for Go) to improve test readability and maintainability through behavior-driven development. Changes: - Convert existing test scenarios to Gherkin feature files - Implement reusable step definitions in steps/steps.go - Add scenario hooks for setup/teardown and feature gate detection - Provide comprehensive documentation in test/e2e/README.md - Remove legacy test files (cluster_extension_install_test.go, etc.) Benefits: - Human-readable test scenarios serve as living documentation - Better separation between test specification and implementation - Easier collaboration between technical and non-technical stakeholders - Reduced code duplication through reusable step definitions Assisted-By: Claude <noreply@anthropic.com>"
6f468f6 to
c272a60
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 20 out of 21 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| ```gherkin | ||
| @WebhookProviderCertManager | ||
| Scenario: Install operator having webhooks |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The feature tag @BoxcutterRuntime is used in the update.feature file but is not documented in the README.md. The README mentions @WebhookProviderCertManager and @SingleOwnNamespaceInstallSupport as examples of feature tags, but @BoxcutterRuntime is missing.
Consider adding this tag to the documentation or verifying that it's initialized in the featureGates map in hooks.go.
| Scenario: Install operator having webhooks | |
| Scenario: Install operator having webhooks | |
| @BoxcutterRuntime | |
| Scenario: Test Boxcutter runtime feature |
| return | ||
| } | ||
| d := &v1.Deployment{} | ||
| if err := json.Unmarshal([]byte(raw), d); err != nil { |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The BeforeSuite function silently returns on errors without logging or reporting them. This could make it difficult to debug issues during test initialization, especially if feature gate detection fails.
Consider logging errors before returning:
func BeforeSuite() {
logger = zap.New(zap.UseFlagOptions(&logOpts))
raw, err := kubectl("get", "deployment", "-n", olmNamespace, olmDeploymentName, "-o", "json")
if err != nil {
logger.Error(err, "Failed to get OLM deployment for feature gate detection")
return
}
d := &v1.Deployment{}
if err := json.Unmarshal([]byte(raw), d); err != nil {
logger.Error(err, "Failed to unmarshal OLM deployment")
return
}
// ... rest of function
}| return | |
| } | |
| d := &v1.Deployment{} | |
| if err := json.Unmarshal([]byte(raw), d); err != nil { | |
| logger.Error(err, "Failed to get OLM deployment for feature gate detection") | |
| return | |
| } | |
| if err := json.Unmarshal([]byte(raw), d); err != nil { | |
| logger.Error(err, "Failed to unmarshal OLM deployment") |
| if stdErr := string(func() *exec.ExitError { | ||
| target := &exec.ExitError{} | ||
| _ = errors.As(err, &target) | ||
| return target | ||
| }().Stderr); !strings.Contains(stdErr, errMsg) { | ||
| return false | ||
| } | ||
| return true |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential nil pointer dereference when extracting error from errors.As. If errors.As returns false (err is not an ExitError), the anonymous function returns nil, and accessing .Stderr will cause a panic.
Consider this safer approach:
waitFor(ctx, func() bool {
_, err := kubectlWithInput(yamlContent, "apply", "-f", "-")
if err == nil {
return false
}
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return strings.Contains(string(exitErr.Stderr), errMsg)
}
return false
})| if stdErr := string(func() *exec.ExitError { | |
| target := &exec.ExitError{} | |
| _ = errors.As(err, &target) | |
| return target | |
| }().Stderr); !strings.Contains(stdErr, errMsg) { | |
| return false | |
| } | |
| return true | |
| var exitErr *exec.ExitError | |
| if errors.As(err, &exitErr) { | |
| if !strings.Contains(string(exitErr.Stderr), errMsg) { | |
| return false | |
| } | |
| return true | |
| } | |
| return false |
| logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", string(func() *exec.ExitError { | ||
| target := &exec.ExitError{} | ||
| _ = errors.As(err, &target) | ||
| return target | ||
| }().Stderr)) | ||
| } |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential nil pointer dereference when extracting error from errors.As. If errors.As returns false (err is not an ExitError), the anonymous function returns nil, and accessing .Stderr will cause a panic.
Consider this safer approach:
if _, err := kubectl("delete", r.kind, r.name, "-n", sc.namespace); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", string(exitErr.Stderr))
} else {
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "error", err.Error())
}
}| logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", string(func() *exec.ExitError { | |
| target := &exec.ExitError{} | |
| _ = errors.As(err, &target) | |
| return target | |
| }().Stderr)) | |
| } | |
| var exitErr *exec.ExitError | |
| if errors.As(err, &exitErr) { | |
| logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", string(exitErr.Stderr)) | |
| } else { | |
| logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "error", err.Error()) | |
| } |
Description
Replace traditional Go e2e tests with Godog (Cucumber for Go) to improve test readability and maintainability through behavior-driven development.
Benefits:
Changes:
Migration Notes
make test-e2eAssisted-By: Claude noreply@anthropic.com
Reviewer Checklist