From f6510161b577c6777bb33b6f9bbae99f98354a23 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Dec 2017 16:35:00 -0800 Subject: [PATCH] add simple TestServer for integrational tests cases --- src/lib.rs | 1 + src/server.rs | 2 +- src/test/mod.rs | 164 +++++++++++++++++++++++++++++++++++++++++++ tests/test_server.rs | 60 +++------------- 4 files changed, 176 insertions(+), 51 deletions(-) create mode 100644 src/test/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 56240a8cc..e8e93eb57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,6 +117,7 @@ pub mod httpcodes; pub mod multipart; pub mod middlewares; pub mod pred; +pub mod test; pub mod payload; pub use error::{Error, Result, ResponseError}; pub use body::{Body, Binary}; diff --git a/src/server.rs b/src/server.rs index 023ed9916..399115076 100644 --- a/src/server.rs +++ b/src/server.rs @@ -191,7 +191,7 @@ impl HttpServer /// Get addresses of bound sockets. pub fn addrs(&self) -> Vec { - self.sockets.keys().map(|addr| addr.clone()).collect() + self.sockets.keys().cloned().collect() } /// The socket address to bind diff --git a/src/test/mod.rs b/src/test/mod.rs new file mode 100644 index 000000000..e3e420827 --- /dev/null +++ b/src/test/mod.rs @@ -0,0 +1,164 @@ +//! Various helpers for Actix applications to use during testing. + +use std::{net, thread}; +use std::sync::mpsc; + +use actix::{Arbiter, SyncAddress, System, msgs}; +use tokio_core::net::TcpListener; + +use server::HttpServer; +use handler::Handler; +use channel::IntoHttpHandler; +use middlewares::Middleware; +use application::{Application, HttpApplication}; + + +/// The `TestServer` type. +/// +/// `TestServer` is very simple test server that simplify process of writing +/// integrational tests cases for actix web applications. +/// +/// # Examples +/// +/// ```rust +/// # extern crate actix; +/// # extern crate actix_web; +/// # use actix_web::*; +/// # extern crate reqwest; +/// # +/// # fn my_handler(req: HttpRequest) -> HttpResponse { +/// # httpcodes::HTTPOk.response() +/// # } +/// # +/// # fn main() { +/// use actix_web::test::TestServer; +/// +/// let srv = TestServer::new(|app| app.handler(my_handler)); +/// +/// assert!(reqwest::get(&srv.url("/")).unwrap().status().is_success()); +/// # } +/// ``` +pub struct TestServer { + addr: net::SocketAddr, + thread: Option>, + sys: SyncAddress, +} + +impl TestServer { + + /// Start new test server + /// + /// This methos accepts configuration method. You can add + /// middlewares or set handlers for test application. + pub fn new(config: F) -> Self + where F: Sync + Send + 'static + Fn(&mut TestApp<()>), + { + TestServer::with_state(||(), config) + } + + /// Start new test server with custom application state + /// + /// This methos accepts state factory and configuration method. + pub fn with_state(state: FS, config: F) -> Self + where S: 'static, + FS: Sync + Send + 'static + Fn() -> S, + F: Sync + Send + 'static + Fn(&mut TestApp), + { + let (tx, rx) = mpsc::channel(); + + // run server in separate thread + let join = thread::spawn(move || { + let sys = System::new("actix-test-server"); + + let tcp = net::TcpListener::bind("0.0.0.0:0").unwrap(); + let local_addr = tcp.local_addr().unwrap(); + let tcp = TcpListener::from_listener(tcp, &local_addr, Arbiter::handle()).unwrap(); + + HttpServer::new(move || { + let mut app = TestApp::new(state()); + config(&mut app); + app} + ).start_incoming(tcp.incoming(), false); + + tx.send((Arbiter::system(), local_addr)).unwrap(); + let _ = sys.run(); + }); + + let (sys, addr) = rx.recv().unwrap(); + TestServer { + addr: addr, + thread: Some(join), + sys: sys, + } + } + + /// Construct test server url + pub fn url(&self, uri: &str) -> String { + if uri.starts_with('/') { + format!("http://{}{}", self.addr, uri) + } else { + format!("http://{}/{}", self.addr, uri) + } + } + + /// Stop http server + fn stop(&mut self) { + if let Some(handle) = self.thread.take() { + self.sys.send(msgs::SystemExit(0)); + let _ = handle.join(); + } + } +} + +impl Drop for TestServer { + fn drop(&mut self) { + self.stop() + } +} + + +/// Test application helper for testing request handlers. +pub struct TestApp { + app: Option>, +} + +impl TestApp { + fn new(state: S) -> TestApp { + let app = Application::with_state(state); + TestApp{app: Some(app)} + } + + /// Register handler for "/" + pub fn handler>(&mut self, handler: H) { + self.app = Some(self.app.take().unwrap().resource("/", |r| r.h(handler))); + } + + /// Register middleware + pub fn middleware(&mut self, mw: T) -> &mut TestApp + where T: Middleware + 'static + { + self.app = Some(self.app.take().unwrap().middleware(mw)); + self + } +} + +impl IntoHttpHandler for TestApp { + type Handler = HttpApplication; + + fn into_handler(self) -> HttpApplication { + self.app.unwrap().finish() + } +} + +#[doc(hidden)] +impl Iterator for TestApp { + type Item = HttpApplication; + + fn next(&mut self) -> Option { + if let Some(mut app) = self.app.take() { + Some(app.finish()) + } else { + None + } + } +} diff --git a/tests/test_server.rs b/tests/test_server.rs index 14369da37..0c0a6bd75 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -3,48 +3,15 @@ extern crate actix_web; extern crate tokio_core; extern crate reqwest; -use std::{net, thread}; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; -use tokio_core::net::TcpListener; -use actix::*; use actix_web::*; #[test] fn test_serve() { - thread::spawn(|| { - let sys = System::new("test"); - let srv = HttpServer::new( - || vec![Application::new() - .resource("/", |r| r.method(Method::GET).h(httpcodes::HTTPOk))]); - srv.bind("127.0.0.1:58902").unwrap().start(); - sys.run(); - }); - assert!(reqwest::get("http://localhost:58902/").unwrap().status().is_success()); -} - -#[test] -fn test_serve_incoming() { - let loopback = net::Ipv4Addr::new(127, 0, 0, 1); - let socket = net::SocketAddrV4::new(loopback, 0); - let tcp = net::TcpListener::bind(socket).unwrap(); - let addr1 = tcp.local_addr().unwrap(); - let addr2 = tcp.local_addr().unwrap(); - - thread::spawn(move || { - let sys = System::new("test"); - - let srv = HttpServer::new( - || Application::new() - .resource("/", |r| r.method(Method::GET).h(httpcodes::HTTPOk))); - let tcp = TcpListener::from_listener(tcp, &addr2, Arbiter::handle()).unwrap(); - srv.start_incoming(tcp.incoming(), false); - sys.run(); - }); - - assert!(reqwest::get(&format!("http://{}/", addr1)) - .unwrap().status().is_success()); + let srv = test::TestServer::new(|app| app.handler(httpcodes::HTTPOk)); + assert!(reqwest::get(&srv.url("/")).unwrap().status().is_success()); } struct MiddlewareTest { @@ -80,21 +47,14 @@ fn test_middlewares() { let act_num2 = Arc::clone(&num2); let act_num3 = Arc::clone(&num3); - thread::spawn(move || { - let sys = System::new("test"); - - HttpServer::new( - move || vec![Application::new() - .middleware(MiddlewareTest{start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3)}) - .resource("/", |r| r.method(Method::GET).h(httpcodes::HTTPOk))]) - .bind("127.0.0.1:58904").unwrap() - .start(); - sys.run(); - }); - - assert!(reqwest::get("http://localhost:58904/").unwrap().status().is_success()); + let srv = test::TestServer::new( + move |app| app.middleware(MiddlewareTest{start: Arc::clone(&act_num1), + response: Arc::clone(&act_num2), + finish: Arc::clone(&act_num3)}) + .handler(httpcodes::HTTPOk) + ); + + assert!(reqwest::get(&srv.url("/")).unwrap().status().is_success()); assert_eq!(num1.load(Ordering::Relaxed), 1); assert_eq!(num2.load(Ordering::Relaxed), 1); assert_eq!(num3.load(Ordering::Relaxed), 1);