-
-
Notifications
You must be signed in to change notification settings - Fork 325
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
Create a mock Client for testing #429
Comments
One point on the potential breaking change, if we are to do it, we should try our best to ensure the ergonomics of it stay as close to "specify the service generic once, have it be inferred everywhere else" when modifying |
UPDATE:
Maybe I missed something? I’m on my phone right now, so I’ll look into it later. |
If it's possible to use |
UPDATE: The following works (tested 6ae744d): #[cfg(test)]
mod tests {
use kube::{Api, Client};
use futures::pin_mut;
use http::{Request, Response};
use hyper::Body;
use k8s_openapi::api::core::v1::Pod;
use tower_test::mock;
#[tokio::test]
async fn test_mock() {
let (mock_service, handle) = mock::pair::<Request<Body>, Response<Body>>();
let spawned = tokio::spawn(async move {
// Receive a request for pod and respond with some data
pin_mut!(handle);
let (request, send) = handle.next_request().await.expect("service not called");
assert_eq!(request.method(), http::Method::GET);
assert_eq!(request.uri().to_string(), "/api/v1/namespaces/default/pods/test");
let pod: Pod = serde_json::from_value(serde_json::json!({
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "test",
"annotations": { "kube-rs": "test" },
},
"spec": {
"containers": [{ "name": "test", "image": "test-image" }],
}
}))
.unwrap();
send.send_response(
Response::builder()
.body(Body::from(serde_json::to_vec(&pod).unwrap()))
.unwrap(),
);
});
let pods: Api<Pod> = Api::namespaced(Client::new(mock_service), "default");
let pod = pods.get("test").await.unwrap();
assert_eq!(pod.metadata.annotations.unwrap().get("kube-rs").unwrap(), "test");
spawned.await.unwrap();
}
} Another example provided by user (remove I'd like to provide something more convenient, though. We can also decorate the mock service as needed similar to the default service. |
Ah, that is not bad for out of the box! I assumed we had to build something on top of our Service, but I had approached it the annoying way of trying to get all our service layers into the mock, which probably isn't that useful for end-users, but somewhat useful for us when unit testing. Not sure if that's worth pursuing. We can test auth at a unit test level within service at the very least - and that's the layer that probably is most test-worthy. |
Yeah, I'd test layers separately like I did for
If you want, you should be able to define a function that returns the default stack, then apply that layer, something like Similar to how we have that's applied with |
Using async fn mock_get_pod(handle: &mut Handle<Request<Body>, Response<Body>>) {
let (request, send) = handle.next_request().await.expect("Service not called");
let body = match (request.method().as_str(), request.uri().to_string().as_str()) {
("GET", "/api/v1/namespaces/default/pods/test") => {
// Can also use recorded resources by reading from a file.
// Or create entire mock from some file mapping routes to resources.
let pod: Pod = serde_json::from_value(serde_json::json!({
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "test",
"annotations": { "kube-rs": "test" },
},
"spec": {
"containers": [{ "name": "test", "image": "test-image" }],
}
}))
.unwrap();
serde_json::to_vec(&pod).unwrap()
}
_ => panic!("Unexpected API request {:?}", request),
};
send.send_response(Response::builder().body(Body::from(body)).unwrap());
} Used like: #[tokio::test]
async fn test_mock_handle() {
let (mock_service, mut handle) = mock::pair::<Request<Body>, Response<Body>>();
let spawned = tokio::spawn(async move {
mock_get_pod(&mut handle).await;
});
let pods: Api<Pod> = Api::default_namespaced(Client::new(mock_service));
let pod = pods.get("test").await.unwrap();
assert_eq!(pod.annotations().get("kube-rs").unwrap(), "test");
spawned.await.unwrap();
} Now with |
@clux I think we don't need anything else on |
Yeah, I agree. The situation is pretty good now, and improvement is likely to come from helper functions. Side note; It feels really hard to present a nice interface for testing controllers well. So many potential flows to keep track of, even the basic test case would be something like: 1. watchevent input -> 2. reconcile start -> 3. reconcile asks something of the api 4. api mock returns canned response, 5. reconcile decides on action, 6. we verify final request. But yeah, let's close. The core issue (and many other things this issue was originally concerned with) are dealt with 👍 |
Yeah, this will be useful for small unit tests, but I don't think it's realistic to test controller's complex behavior like this. It should be possible to make stateful mocks (like For that, I think we should add a test helper to start a cluster for each test (#382), and some test macros for convenience. |
…we can consume in tests stepwise basic test setup as per kube-rs/kube#429 (comment)
Create a mockable
Client
that allows us to perform fakeApi
calls, intercept the output, and send fake responses to it.Why?:
kube::Api
andkube_runtime
kube::Api
andkube_runtime
by faking a few responsesAsked for in #426.
This is something that is theoretically possible through something like
tower_test
, but it brings with it complications:Client
would need to take two types ofService
's (one of which intercepts calls and allows shoehorning in responses)MultipleClient
types => generic parameter required inApi
(since it houses theClient
) - discussed in sans-io Client #406The latter is a huge breaking change (though not necessarily against it unless it can be avoided)tower_test::Mock
This is a more important (and concrete) want than what is nebulously outlined in #406 - so we probably close that in favour of this testing issue.
UPDATE:
Client
takesService<http::Request<hyper::Body>>
(#532) so this is easier now.The text was updated successfully, but these errors were encountered: