Distinguish feature/assessment reason from name
negz opened this issue · 6 comments
I'm working on porting Crossplane's nascent E2E tests to e2e-framework in crossplane/crossplane#4187. I find the features.Table
approach more readable than the builder pattern, so most of my tests look like this:
func TestConfiguration(t *testing.T) {
// Test that we can install a Configuration from a private repository using
// a package pull secret.
manifests := "test/e2e/manifests/pkg/configuration/private"
private := features.Table{
{
Name: "ConfigurationIsCreated",
Assessment: funcs.AllOf(
funcs.ApplyResources(FieldManager, manifests, "*.yaml"),
funcs.ResourcesCreatedWithin(1*time.Minute, manifests, "*.yaml"),
),
},
// etc...
}
setup := funcs.ReadyToTestWithin(1*time.Minute, namespace)
environment.Test(t,
private.Build("PullFromPrivateRegistry").
WithLabel("area", "pkg").
WithLabel("size", "small").
Setup(setup).Feature(),
)
I've noticed that a lot of folks like to use descriptive prose to "name" their assessments and features. So more like:
func TestConfiguration(t *testing.T) {
manifests := "test/e2e/manifests/pkg/configuration/private"
private := features.Table{
{
Name: "The supplied Configuration should be created",
Assessment: funcs.AllOf(
funcs.ApplyResources(FieldManager, manifests, "*.yaml"),
funcs.ResourcesCreatedWithin(1*time.Minute, manifests, "*.yaml"),
),
},
// etc...
}
setup := funcs.ReadyToTestWithin(1*time.Minute, namespace)
environment.Test(t,
private.Build("Test that we can install a Configuration from a private repository using a package pull secret.").
WithLabel("area", "pkg").
WithLabel("size", "small").
Setup(setup).Feature(),
)
I like this in theory - I think it's a good idea to provide as much written context as possible for anyone reading the test code and/or debugging why the test has failed. In practice, I stick with PascalCase
assessment and feature names, because they're actually t.Run
subtest names. Go mangles them into Really_really_long/nested_snake_case_names
which I find ugly and hard to read:
--- PASS: TestConfiguration (60.15s)
--- PASS: TestConfiguration/Pull_a_Configuration_from_a_private_registry (30.07s)
--- PASS: TestConfiguration/Pull_a_Configuration_from_a_private_registry/The_supplied_Configuration_should_be_created (5.03s)
--- PASS: TestConfiguration/Pull_a_Configuration_from_a_private_registry/The_supplied_configuration_should_become_healthy,_and_active (5.01s)
--- PASS: TestConfiguration/Pull_a_Configuration_from_a_private_registry/The_supplied_Configuration_should_be_deleted (5.03s)
--- PASS: TestConfiguration/Pull_a_Configuration_that_depends_on_a_Provider (30.08s)
--- PASS: TestConfiguration/Pull_a_Configuration_that_depends_on_a_Provider/The_supplied_Configuration_should_be_created (5.02s)
--- PASS: TestConfiguration/Pull_a_Configuration_that_depends_on_a_Provider/The_supplied_Configuration_should_become_healthy,_and_active (5.01s)
--- PASS: TestConfiguration/Pull_a_Configuration_that_depends_on_a_Provider/The_Provider_the_Configuration_depends_on_should_become_healthy,_and_active (5.01s)
--- PASS: TestConfiguration/Pull_a_Configuration_that_depends_on_a_Provider/The_supplied_Configuration_should_be_deleted (5.01s)
--- PASS: TestConfiguration/Pull_a_Configuration_that_depends_on_a_Provider/The_supplied_Provider_should_be_deleted_(providers_are_not_deleted_automatically) (5.01s)
What do you think about allowing folks to provide a feature or assessment's written context, distinct from the name of the feature or assessment? For example:
func TestConfiguration(t *testing.T) {
manifests := "test/e2e/manifests/pkg/configuration/private"
private := features.Table{
{
Name: "CreateConfiguration",
Reason: "We should be able to install a Configuration from a private repository using a package pull secret",
Assessment: funcs.AllOf(
funcs.ApplyResources(FieldManager, manifests, "*.yaml"),
funcs.ResourcesCreatedWithin(1*time.Minute, manifests, "*.yaml"),
),
},
// etc...
}
setup := funcs.ReadyToTestWithin(1*time.Minute, namespace)
environment.Test(t,
private.Build("PullFromPrivateRegistry").
Reason("Crossplane should support pulling packages from private repositories").
WithLabel("area", "pkg").
WithLabel("size", "small").
Setup(setup).Feature(),
)
I imagine the Reason
field would be emitted via t.Log
when a test started, and perhaps somehow included (regardless of test verbosity) if a test failed. I first saw a pattern like this in the go-cmp unit tests, and we adopted it a while back for Crossplane unit tests to good effect.
@negz I think this would be really nice to have in place. Having a reason/description can always add more context to a test failure and the reason why certain test is being done. Let us see what @vladimirvivien @ShwethaKumbla @cpanato have to say about this.
diff --git a/pkg/env/env.go b/pkg/env/env.go
index 8055010..0f47301 100644
--- a/pkg/env/env.go
+++ b/pkg/env/env.go
@@ -441,6 +441,11 @@ func (e *testEnv) execFeature(ctx context.Context, t *testing.T, featName string
failed := false
for i, assess := range assessments {
+ if dAssess, ok := assess.(types.DescribableStep); ok {
+ if dAssess.Description() != "" {
+ klog.V(4).InfoS("Processing Assessment", "description", dAssess.Description())
+ }
+ }
assessName := assess.Name()
if assessName == "" {
assessName = fmt.Sprintf("Assessment-%d", i+1)
diff --git a/pkg/features/builder.go b/pkg/features/builder.go
index a6fa403..f5936f0 100644
--- a/pkg/features/builder.go
+++ b/pkg/features/builder.go
@@ -71,6 +71,11 @@ func (b *FeatureBuilder) Assess(desc string, fn Func) *FeatureBuilder {
return b.WithStep(desc, types.LevelAssess, fn)
}
+func (b *FeatureBuilder) AssessWithDescription(name, description string, fn Func) *FeatureBuilder {
+ b.feat.steps = append(b.feat.steps, newStepWithDescription(name, description, types.LevelAssess, fn))
+ return b
+}
+
// Feature returns a feature configured by builder.
func (b *FeatureBuilder) Feature() types.Feature {
return b.feat
diff --git a/pkg/features/feature.go b/pkg/features/feature.go
index 99ba103..6b32261 100644
--- a/pkg/features/feature.go
+++ b/pkg/features/feature.go
@@ -53,9 +53,10 @@ func (f *defaultFeature) Steps() []types.Step {
}
type testStep struct {
- name string
- level Level
- fn Func
+ description string
+ name string
+ level Level
+ fn Func
}
func newStep(name string, level Level, fn Func) *testStep {
@@ -66,6 +67,15 @@ func newStep(name string, level Level, fn Func) *testStep {
}
}
+func newStepWithDescription(name, description string, level Level, fn Func) *testStep {
+ return &testStep{
+ description: description,
+ name: name,
+ level: level,
+ fn: fn,
+ }
+}
+
func (s *testStep) Name() string {
return s.name
}
@@ -78,6 +88,10 @@ func (s *testStep) Func() Func {
return s.fn
}
+func (s *testStep) Description() string {
+ return s.description
+}
+
func GetStepsByLevel(steps []types.Step, l types.Level) []types.Step {
if steps == nil {
return nil
diff --git a/pkg/features/table.go b/pkg/features/table.go
index d2afed9..242b6e9 100644
--- a/pkg/features/table.go
+++ b/pkg/features/table.go
@@ -23,8 +23,9 @@ import (
// Table provides a structure for table-driven tests.
// Each entry in the table represents an executable assessment.
type Table []struct {
- Name string
- Assessment Func
+ Name string
+ Description string
+ Assessment Func
}
// Build converts the defined test steps in the table
@@ -42,7 +43,7 @@ func (table Table) Build(featureName ...string) *FeatureBuilder {
test.Name = fmt.Sprintf("Assessment-%d", i)
}
if test.Assessment != nil {
- f.Assess(test.Name, test.Assessment)
+ f.AssessWithDescription(test.Name, test.Description, test.Assessment)
}
}
return f
diff --git a/pkg/internal/types/types.go b/pkg/internal/types/types.go
index 657931f..41d7664 100644
--- a/pkg/internal/types/types.go
+++ b/pkg/internal/types/types.go
@@ -119,3 +119,10 @@ type Step interface {
// Func is the operation for the step
Func() StepFunc
}
+
+type DescribableStep interface {
+ Step
+
+ // Description is the readable information indicating what the step is performing
+ Description() string
+}
I managed to make a few quick changes to see if we can really achieve what we really want in terms of being able to use a description without breaking the existing contracts so that we can continue the discussion further.
❯ ./k --v=7 -test.v
=== RUN TestTableDriven
=== RUN TestTableDriven/Random_numbers
I0620 00:22:11.613167 72002 env.go:446] "Processing Assessment" description="This is a fun test"
=== RUN TestTableDriven/Random_numbers/less_than_equal_64
table_test.go:55: limit should be less than 64
=== RUN TestTableDriven/Random_numbers/more_than_than_equal_128
=== RUN TestTableDriven/Random_numbers/Assessment-2
=== RUN TestTableDriven/Feature-2
=== RUN TestTableDriven/Feature-2/A_simple_feature
table_test.go:90: this is a great number
--- PASS: TestTableDriven (0.00s)
--- PASS: TestTableDriven/Random_numbers (0.00s)
--- PASS: TestTableDriven/Random_numbers/less_than_equal_64 (0.00s)
--- PASS: TestTableDriven/Random_numbers/more_than_than_equal_128 (0.00s)
--- PASS: TestTableDriven/Random_numbers/Assessment-2 (0.00s)
--- PASS: TestTableDriven/Feature-2 (0.00s)
--- PASS: TestTableDriven/Feature-2/A_simple_feature (0.00s)
PASS
I am yet to find out the best way to integrate this description into failure message.
I am yet to find out the best way to integrate this description into failure message.
@harshanarayana Perhaps we don't need to. go test
will automatically include all t.Log
statements associated with a particular test if it fails, even when it's running without -test.v
. So if you just keep the t.Log
statement you've already added:
- When running with
-test.v
it will be logged at the start of every test (regardless of pass or fail) - When running without
-test.v
It will be logged at the start of any test that fails.
I think that's good enough - if a test fails you'll see the log statement describing what the test is doing.
@negz Valid point. I will switch to using t.Log
instead of the klog for these descriptions so that they are managed by the go test accordingly based on failures/success and log levels. @vladimirvivien If you think this is a safe approach, I will open a PR for review.
@negz and @harshanarayana This is beautiful ❤️
I really don't have much to add.
@negz Awesome to see someone using the Table
construct.
@vladimirvivien @negz I have opened a PR under #284 to discuss this further and get the changes done. PTAL when possible.