Unit Testing
Unit testing is a software testing method where individual units of code are tested in isolation. For KubeOps operators, this means testing controllers, finalizers, and webhooks without requiring a running Kubernetes cluster.
Testing Approach
KubeOps operators can be tested using standard .NET testing frameworks and mocking libraries. The key is to:
- Mock dependencies (like
IKubernetesClient
,EventPublisher
, etc.) - Create test entities
- Call methods directly
- Verify the expected behavior
Testing Controllers
Here's an example of testing a controller using xUnit and Moq:
public class DemoControllerTests
{
private readonly Mock<IKubernetesClient> _clientMock;
private readonly Mock<EventPublisher> _eventPublisherMock;
private readonly DemoController _controller;
public DemoControllerTests()
{
_clientMock = new Mock<IKubernetesClient>();
_eventPublisherMock = new Mock<EventPublisher>();
_controller = new DemoController(_clientMock.Object, _eventPublisherMock.Object);
}
[Fact]
public async Task ReconcileAsync_WhenEntityIsNew_CreatesDeployment()
{
// Arrange
var entity = new V1DemoEntity
{
Metadata = new V1ObjectMeta
{
Name = "test-entity",
NamespaceProperty = "default"
},
Spec = new V1DemoEntitySpec
{
Replicas = 3
}
};
_clientMock
.Setup(c => c.Get<V1Deployment>(entity.Metadata.Name, entity.Metadata.NamespaceProperty))
.ReturnsAsync((V1Deployment)null);
// Act
await _controller.ReconcileAsync(entity, CancellationToken.None);
// Assert
_clientMock.Verify(
c => c.Create(It.Is<V1Deployment>(d =>
d.Metadata.Name == entity.Metadata.Name &&
d.Spec.Replicas == entity.Spec.Replicas)),
Times.Once);
_eventPublisherMock.Verify(
e => e(
entity,
"Created",
It.IsAny<string>(),
EventType.Normal,
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task ReconcileAsync_WhenDeploymentExists_UpdatesReplicas()
{
// Arrange
var entity = new V1DemoEntity
{
Metadata = new V1ObjectMeta
{
Name = "test-entity",
NamespaceProperty = "default"
},
Spec = new V1DemoEntitySpec
{
Replicas = 5
}
};
var existingDeployment = new V1Deployment
{
Metadata = new V1ObjectMeta
{
Name = entity.Metadata.Name,
NamespaceProperty = entity.Metadata.NamespaceProperty
},
Spec = new V1DeploymentSpec
{
Replicas = 3
}
};
_clientMock
.Setup(c => c.Get<V1Deployment>(entity.Metadata.Name, entity.Metadata.NamespaceProperty))
.ReturnsAsync(existingDeployment);
// Act
await _controller.ReconcileAsync(entity, CancellationToken.None);
// Assert
_clientMock.Verify(
c => c.Update(It.Is<V1Deployment>(d =>
d.Metadata.Name == entity.Metadata.Name &&
d.Spec.Replicas == entity.Spec.Replicas)),
Times.Once);
}
}
Testing Finalizers
Testing finalizers follows a similar pattern:
public class DemoFinalizerTests
{
private readonly Mock<IKubernetesClient> _clientMock;
private readonly DemoFinalizer _finalizer;
public DemoFinalizerTests()
{
_clientMock = new Mock<IKubernetesClient>();
_finalizer = new DemoFinalizer(_clientMock.Object);
}
[Fact]
public async Task FinalizeAsync_WhenResourcesExist_DeletesThem()
{
// Arrange
var entity = new V1DemoEntity
{
Metadata = new V1ObjectMeta
{
Name = "test-entity",
NamespaceProperty = "default"
}
};
var resources = new List<V1Deployment>
{
new() { Metadata = new V1ObjectMeta { Name = "resource-1" } },
new() { Metadata = new V1ObjectMeta { Name = "resource-2" } }
};
_clientMock
.Setup(c => c.List<V1Deployment>(It.IsAny<string>()))
.ReturnsAsync(resources);
// Act
await _finalizer.FinalizeAsync(entity, CancellationToken.None);
// Assert
_clientMock.Verify(
c => c.Delete(It.IsAny<V1Deployment>()),
Times.Exactly(2));
}
}
Testing Webhooks
Webhooks can be tested by verifying their validation or mutation logic:
public class DemoValidationWebhookTests
{
private readonly DemoValidationWebhook _webhook;
public DemoValidationWebhookTests()
{
_webhook = new DemoValidationWebhook();
}
[Fact]
public void Create_WhenUsernameIsForbidden_ReturnsError()
{
// Arrange
var entity = new V1DemoEntity
{
Spec = new V1DemoEntitySpec
{
Username = "forbidden"
}
};
// Act
var result = _webhook.Create(entity, false);
// Assert
Assert.False(result.Success);
Assert.Contains("forbidden", result.Message);
}
[Fact]
public void Create_WhenUsernameIsValid_ReturnsSuccess()
{
// Arrange
var entity = new V1DemoEntity
{
Spec = new V1DemoEntitySpec
{
Username = "valid-user"
}
};
// Act
var result = _webhook.Create(entity, false);
// Assert
Assert.True(result.Success);
}
}
Best Practices
-
Test Organization:
- Group tests by component (controllers, finalizers, webhooks)
- Use descriptive test names
- Follow the Arrange-Act-Assert pattern
-
Mocking:
- Mock only what's necessary
- Verify important interactions
- Use strict mocks when appropriate
-
Test Coverage:
- Test success and failure cases
- Test edge cases
- Test error handling
-
Test Data:
- Use realistic test data
- Create helper methods for common setups
- Consider using test data builders
Common Pitfalls
-
Over-mocking:
- Don't mock everything
- Focus on external dependencies
- Keep tests simple
-
Test Maintenance:
- Keep tests focused
- Avoid test interdependence
- Document complex test scenarios
-
Test Reliability:
- Avoid timing-dependent tests
- Use deterministic test data
- Clean up test resources