Repository navigation
Example test server #1178
|
I see that in a previous question that the constructor of ureq implements Http so that you can create a mock instance. I may have missed it, but would it be useful to create an example of this in the documentation to get people started? Thank you in advance! |
Replies: 4 comments 3 replies
|
@bitterepic could this have been about the previous ureq 2.x response constructor? I can't think of what you mean in 3.x. Which previous question are you talking about? |
|
Sorry for the wait! For simplicity I ended up using a local webserver that is spun up for each of my tests. It seems to work well and fast... would it be worth adding it to the examples of the project? Or is this a bit weird? Example NOTE CODE HAS BEEN UPDATED AT THE END OF THE THREAD. |
|
This is an example of a custom webserver that you can have start on each test. It is meant as a simple starting point for someone who might want something more custom. This code has no external dependencies outside of serde/serde_json/ureq. It follows the patterns as given in the official rust book as seen here. Given this basic server:use serde_json::json;
use std::thread::spawn;
use std::{
io::{BufReader, prelude::*},
net::TcpListener,
sync::Arc,
sync::RwLock,
};
#[derive(Debug)]
pub struct TestServerRequest {
pub route: String,
#[allow(dead_code)]
pub pathname: String,
#[allow(dead_code)]
pub method: String,
}
fn create_body<S: AsRef<str>, T: AsRef<str>>(status_code: u32, status_text: S, body: T) -> String {
let body = body.as_ref();
let status_text = status_text.as_ref();
let length = body.len();
[
format!("HTTP/1.1 {status_code} {status_text}"),
"Content-Type: application/json".to_string(),
format!("Content-Length: {length}"),
"".to_string(),
body.to_string(),
]
.join("\r\n")
}
pub type TestServerHandler = fn(TestServerRequest) -> Option<String>;
pub struct TestServer {
port: u16,
host: String,
listener: Arc<TcpListener>,
running: Arc<RwLock<bool>>,
handler: Arc<RwLock<Option<TestServerHandler>>>,
}
// Stop the server when the test server instance is dropped
impl Drop for TestServer {
fn drop(&mut self) {
self.stop();
}
}
impl TestServer {
/// Create a new server. You must set the routes and call start to start it.
/// EXAMPLE:
/// let server = TestServer::new();
/// server.start();
/// server.routes(|request| {
/// match request.route.as_ref() {
/// "GET:/ping" => Some(json!("ping").to_string()),
/// _ => None, // A 404 page
/// }
/// });
pub fn new() -> Self {
let host = "localhost".to_string();
let address = format!("{host}:0");
let listener = TcpListener::bind(&address).unwrap();
let port = listener.local_addr().unwrap().port();
let mut out = Self {
listener: Arc::new(listener),
port,
host,
handler: Arc::new(RwLock::new(None)),
running: Arc::new(RwLock::new(true)),
};
out.start();
out
}
fn stop(&self) {
{
*self.running.write().unwrap() = false;
}
// Make the server runs one more loop to
// call break and end the loop.
let _ = ureq::get(self.base_url()).call();
}
/// The base url for the server. You will need it to call api calls against.
pub fn base_url(&self) -> String {
let host = &self.host;
let port = &self.port;
format!("http://{host}:{port}")
}
/// A helper to create a url form the base_url.
/// EXAMPLE
/// server.url("/ping")
pub fn url<S: AsRef<str>>(&self, pathname: S) -> String {
[&self.base_url(), pathname.as_ref()].join("")
}
/// Starts the server thread.
fn start(&mut self) {
let handler = Arc::clone(&self.handler);
let listener = Arc::clone(&self.listener);
let running = Arc::clone(&self.running);
let base_url = self.base_url();
log::info!("Starting server {}", base_url);
spawn(move || {
for stream in listener.incoming() {
if !*running.read().unwrap() {
log::info!("Qutting server {}", base_url);
break;
}
let mut stream = stream.unwrap();
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (method, pathname) = {
let parts: Vec<&str> = request_line.split(" ").collect();
let method = parts[0..1].join("");
let pathname = parts[1..parts.len() - 1].join(" ");
(method, pathname)
};
let route = [method.to_string(), pathname.to_string()].join(":");
let request = TestServerRequest {
route,
pathname,
method,
};
if let Some(handler) = *(handler.read().unwrap()) {
if let Some(body) = handler(request) {
let response = create_body(200, "OK", body);
stream.write_all(response.as_bytes()).unwrap();
} else {
let body = json!({ "message": "Not Found" }).to_string();
let response = create_body(404, "NOT FOUND", body);
stream.write_all(response.as_bytes()).unwrap();
}
}
}
});
}
/// Sets the routes for the server thread. You can call this as many times as you like.
pub fn routes(&mut self, handler: fn(TestServerRequest) -> Option<String>) {
*self.handler.write().unwrap() = Some(handler);
}
}You can run tests with this format#[cfg(test)]
mod test {
use super::*;
fn setup() -> TestServer {
let mut out = TestServer::new();
out.start();
out
}
#[test]
fn test() {
let mut server = setup();
server.routes(|request| {
match request.route.as_ref() {
"GET:/ping" => Some(json!("ping").to_string()),
_ => None, // A 404 page
}
});
// Test a set response
let response: String = ureq::get(server.url("/ping"))
.call()
.unwrap()
.body_mut()
.read_json()
.unwrap();
assert_eq!(response, "ping");
// Paths that return None will return a 404
match ureq::get(server.url("/404")).call().unwrap_err() {
ureq::Error::StatusCode(status_code) => assert_eq!(status_code, 404),
_ => panic!("Not reachable"),
};
}
}Zipped source code. |
|
@algesten I'll close this with the marked updated answer. The code is fit enough for others to look at now and hopefully be educational without being too comprehensive. |
This is an example of a custom webserver that you can have start on each test. It is meant as a simple starting point for someone who might want something more custom. This code has no external dependencies outside of serde/serde_json/ureq. It follows the patterns as given in the official rust book as seen here.
Given this basic server: