Analysis

Rust testing guide shows how to mock Kubernetes API calls

A Rust controller broke when Kubernetes events turned out to be real API calls. The fix is less about mocking harder and more about designing the boundary better.

Sam Ortega··5 min read
Published
Listen to this article0:00 min
Share this article:
Rust testing guide shows how to mock Kubernetes API calls
Source: blog.appliedcomputing.io

A Rust controller that looked harmless in tests turned brittle the moment it started emitting Kubernetes events. The code was not printing extra noise, it was making real requests to the Kubernetes API server, and the unit tests failed because nothing was listening on the other end.

That is the sharp edge this guide gets right. Kubernetes events are API objects, not console output, and they ride through the apiserver, which Kubernetes documentation describes as the frontend to the cluster’s shared state. They are also short-lived by design, commonly used for monitoring, troubleshooting, and alerting, and typically retained for only about one hour unless exported elsewhere.

Why Kubernetes controllers make testing awkward

The controller model in kube-rs is built around reconciliation, which is exactly why the pain shows up so quickly. A `Controller` watches resources from the Kubernetes API, maps related objects into the main object, runs a user-defined `reconcile` function, and then observes the result to decide when to reschedule. In practice, that means your code is always sitting on a network boundary, even when the business logic feels local.

kube-rs is not pretending this is easy. Its docs warn that Kubernetes watch delivery is not guaranteed, so controller logic has to stay defensive and avoid depending on any particular reconcile reason. That matters for Rust infrastructure code because safety at the type level does not remove the need to test behavior against external systems.

The broader context is familiar if you have lived in Go controller land. kube-rs positions itself as a Rust Kubernetes client and controller runtime, with an abstraction inspired by client-go and controller-runtime. Rust gives you stronger guarantees inside the process, but the minute you talk to a cluster, the same integration problems show up.

Treat the testing guide like a decision tree, not a slogan

The kube-rs testing guide is useful because it breaks the problem into real categories instead of hand-waving. It distinguishes end-to-end tests, integration tests, mocked unit tests, and plain unit tests. It also says the controller is fundamentally tied up in the reconciler, so the right test depends on how much of the real Kubernetes behavior you need to exercise.

There are three main moves here. You can stay in unit-test land and mock out network dependencies, move up the test pyramid and do full-scale integration testing, or stand up a Kubernetes dependency that behaves enough like the real thing to keep the controller honest. For controllers and operators, that standing-in dependency is often the difference between a test that proves something and a test that just passes in a fantasy world.

A practical way to think about it looks like this:

  • If you are checking pure decision logic, keep the boundary small and test the reconcile outcomes directly.
  • If you are checking how your code talks HTTP, use a local mock server and verify requests and responses.
  • If you need Kubernetes semantics, use a mock apiserver, an in-memory client, or a real cluster.
  • If the behavior spans scheduling, watch events, and resync style flow, move closer to integration testing instead of piling on mocks.

Choose the mock that matches the failure

The guide names the tools that actually show up in Rust projects: wiremock, mockito, tower-test, and mockall. That list is telling, because it covers different layers of the problem rather than one universal answer. tower-test gets special emphasis because it fits kube’s client stack well, while mockall is the obvious pick when you want trait-driven mocks and wiremock or mockito make sense when the boundary is plain HTTP.

The important lesson is not the tool list itself. It is that you should mock the layer your code really depends on, not the one that is easiest to fake. If your controller logic only needs to know whether an API call succeeded, a trait-based fake is cleaner than stubbing every method on the real client. If the bug lives in the request shape, a local test server is better because it lets you inspect the actual call instead of assuming the trait abstraction captured it.

That is also where kubernetes-mock-rs fits nicely. It exists specifically as an in-memory mock Kubernetes client for testing controllers and operators in Rust, which makes it a natural fit when you need more Kubernetes flavor than a generic HTTP stub can provide but do not want to pay for a live cluster on every test run.

When the right answer is to redesign the boundary

The strongest point in the guide is implicit, but it matters more than any single library: testability is not just mocking an interface after the fact. It is designing the code so production behavior can be exercised without forcing the test suite to weaken the abstraction that made the code worthwhile in the first place.

That usually means separating side effects from reconciliation decisions, pushing external calls behind narrow traits, and making the controller’s core logic small enough that it can be exercised without a full Kubernetes stack. If the code is impossible to test unless you fake half the world, the issue is rarely the test harness. It is usually the shape of the code.

This is where the Kubernetes event example earns its keep. The problem was never that Rust could not mock something. The problem was that a supposedly simple behavior was actually an API call into the apiserver, and the test suite had been written as if it were just another log line.

The takeaway for Rust teams building infrastructure

The most useful thing about this guide is that it treats controller testing like a systems problem, not a language exercise. Kubernetes events are real, ephemeral API objects. Controllers are reconcile loops with network edges. Watch delivery is not guaranteed. Once you accept those facts, the mocking strategy becomes clearer, and the test suite gets much closer to the production code it is meant to protect.

That first broken test was a warning, not a dead end. If the code is talking to a real apiserver in production, the test strategy has to respect that boundary too, or the next “easy” event will break the suite in exactly the same way.

Know something we missed? Have a correction or additional information?

Submit a Tip

Never miss a story.

Get Rust Programming updates weekly. The top stories delivered to your inbox.

Free forever · Unsubscribe anytime

Discussion

More Rust Programming News