-
Notifications
You must be signed in to change notification settings - Fork 6
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
Test subnet controller #177
Conversation
21be879
to
73eca72
Compare
73eca72
to
06ac011
Compare
1cdad42
to
dfb8d81
Compare
controllers/network/suite_test.go
Outdated
RegisterFailHandler(Fail) | ||
|
||
RunSpecsWithDefaultAndCustomReporters(t, | ||
"Controller Suite", |
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.
Change the name to "Network Controller Suite"? Because we have multiple subpackages in controllers
package.
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.
Agreed
"reflect" | ||
"time" | ||
|
||
"github.com/onmetal/onmetal-api/apis/common/v1alpha1" |
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.
Let's use goimports
for the code with --local github.com/onmetal/onmetal-api
flag. what you think?
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.
Agreed. For VSCode users, add the following to settings.json.
"go.formatTool": "goimports",
"gopls": {
"formatting.local": "github.com/onmetal/onmetal-api",
},
) | ||
|
||
var _ = Describe("subnet controller", func() { | ||
Context("reconcileExists", func() { |
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 would merge it with "Reconcile" context.
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.
My idea was:
Context
: function name
It
: what the function does
By
: steps in the function
Like here it's more like a function name to me.
Sill open to discussion.
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 whole point of using Ginkgo is to implement behavior driven testing. So we don't need to use function names in organizational units(i.e. Context
, It
, etc). Instead we mention behavior. Like Context("Reconcile")
, which, at least usually, is top level definition of testable behavior in controller tests.
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.
P.S. I also, would say, that unit testing is undesirable, as theoretically it can be broken without breaking desired behavior. It's always preferred to test general behavior of the component)
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 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.
What do you mean by this?
it can be broken without breaking desired behavior
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.
What do you mean by this?
it can be broken without breaking desired behavior
For example, some refactoring is done, part of functionality moved to another function called from calling function. Unit test will be broken, general behavior will not.
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.
Point is, we test behavior not functions)
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.
A function name should describe the behaviour in short. I don't see a conflict here. Yet, since we don't have finalizers here and don't have to do some clean-up after the deletion, the context reconcileExists is indeed too thin.
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.
Some required changes. In general, try to avoid Ginkgo assertions that yield bool
: If they fail, you'll get an error message like
expected 'true' to be 'false'
or else, which isn't very helpful. The richer Ginkgo assertions give back understandable messages which makes troubleshooting a lot easier. If you ever can't get around boolean assertions, make sure to include a hint in the matcher, like
Expect(myResult).To(BeTrue(), "the 'my-operation' operation did not succeed")
|
||
Eventually(func() bool { | ||
rngGot := &nw.IPAMRange{} | ||
Expect(k8sClient.Get(ctx, objectKey(ipamRange), rngGot)) |
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.
.To(Succeed())
assertion is missing
Eventually(func() bool { | ||
rngGot := &nw.IPAMRange{} | ||
Expect(k8sClient.Get(ctx, objectKey(ipamRange), rngGot)) | ||
return rngGot.Spec.CIDRs == 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.
I'd do
return rngGot.Spec.CIDRs
And then as assertion, instead of BeTrue
BeEmpty
or BeNil
. In case the slice is not empty, there will be a better error message what the contained items are.
It("finishes reconciliation early if the instance is being deleted", func() { | ||
subnet := newSubnet("early-finished") | ||
subnet.DeletionTimestamp = now() | ||
ipamRange := newIPAMRange(subnet) |
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 should not create the IPAMRange
ourselves and instead wait for the Subnet
controller to create its IPAMRange
object (it does so via server-side apply).
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.
OK! I didn't get this bit and thought the IPAMRange is already generated and only get patched by Subnet controller. Thanks!
Context("reconcileExists", func() { | ||
It("finishes reconciliation early if the instance is being deleted", func() { | ||
subnet := newSubnet("early-finished") | ||
subnet.DeletionTimestamp = 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.
Instead of creating it w/ a deletion timestamp set to now, we should create it, wait for the IPAMRange to be created and then delete it to see if it's gone afterwards (it should be gone due to us correctly setting up owner references).
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.
Due to the limit of envtest only the owner reference of the owned IPAMRange is checked.
subnet := newSubnet("no-parant") | ||
ipamRange := newIPAMRange(subnet) | ||
|
||
Expect(k8sClient.Create(ctx, ipamRange)).Should(Succeed()) |
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 shouldn't create the IPAMRange
manually and instead let the controller do this.
Expect(k8sClient.Create(ctx, parent)).Should(Succeed()) | ||
Expect(k8sClient.Create(ctx, child)).Should(Succeed()) | ||
|
||
By("patching the spec of the owned IPAMRange") |
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.
By("waiting for the parent IPAMRange to have an allocation by the child IPAMRange")
return func() bool { | ||
parentRng := rngGot.Spec.Parent | ||
parentSubnet := child.Spec.Parent | ||
if parentRng == nil || parentSubnet == nil { | ||
return false | ||
} | ||
|
||
// Check if the IPAMRange is patched | ||
return parentRng.Name == nw.SubnetIPAMName(parentSubnet.Name) && | ||
rngGot.Spec.CIDRs == nil && | ||
reflect.DeepEqual(rngGot.Spec.Requests[0], nw.IPAMRangeRequest{CIDR: &child.Spec.Ranges[0].CIDR}) | ||
|
||
}() | ||
}, timeout, interval).Should(BeTrue()) |
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.
Instead of this block, just use
return rngGot.Spec.Requests
}, timeout, interval).Should(ContainElement(child.Spec.Ranges[0].CIDR})
}() | ||
}, timeout, interval).Should(BeTrue()) | ||
|
||
By("patching the status of the Subnet") |
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.
By("waiting for the subnet to report status 'up'")
+ modify the assertion as described in the parent-less example.
controllers/network/suite_test.go
Outdated
func TestAPIs(t *testing.T) { | ||
RegisterFailHandler(Fail) | ||
|
||
RunSpecsWithDefaultAndCustomReporters(t, |
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.
Use RunSpecs
and remove the custom reporter. That feature won't be available in the latest Ginkgo version anymore.
controllers/network/suite_test.go
Outdated
var _ = BeforeSuite(func() { | ||
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) | ||
|
||
ctx, cancel = context.WithCancel(context.TODO()) |
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.
context.TODO()
-> context.Background()
}).SetupWithManager(k8sManager) | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
err = (&IPAMRangeReconciler{ |
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 a question not a call to action) Is IPAMRangeReconciler
needed for testing SubnetReconciler
?
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.
At the moment, no, but eventually, yes. The reconciler of Subnet will check the status of the IPAMRange and update the Subnet accordingly. (here) There might be a bug here which I will address in another issue.
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.
Some minor changes, otherwise looks pretty good!
|
||
ipamRng := newIPAMRange(subnet) | ||
Eventually(func() error { | ||
return k8sClient.Get(ctx, objectKey(ipamRng), ipamRng) |
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.
Two problems here:
- If the
Get
ever fails, theipamRng
object might be reset, deleting itsmetadata
, resulting in theobjectKey
returning an emptyclient.ObjectKey
, resulting in always gettingNot Found
. The key should be created outside thefunc()
. - On errors other than
apierrors.IsNotFound
, the test should error early, as that's a non-recoverable error.
Please make sure to check allGet
+Eventually
combinations.
ipamRng := newIPAMRange(subnet) | ||
Eventually(func() error { | ||
return k8sClient.Get(ctx, objectKey(ipamRng), ipamRng) | ||
}, timeout, interval).Should(BeNil()) |
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.
BeNil()
-> Succeed()
Closes #176