From 743235b8fd3a21a522435977cb0746f93bdf5b22 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Dec 2017 19:48:02 -0800 Subject: [PATCH] add unit test helper --- guide/src/qs_8.md | 37 +++++++++ src/application.rs | 5 +- src/handler.rs | 9 +- src/httprequest.rs | 54 ++---------- src/test/mod.rs | 202 ++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 253 insertions(+), 54 deletions(-) diff --git a/guide/src/qs_8.md b/guide/src/qs_8.md index be08e755..7661b9ad 100644 --- a/guide/src/qs_8.md +++ b/guide/src/qs_8.md @@ -3,6 +3,43 @@ Every application should be well tested and. Actix provides the tools to perform unit and integration tests. +## Unit tests + +For unit testing actix provides request builder type and simple handler runner. +[*TestRequest*](../actix_web/test/struct.TestRequest.html) implements builder-like pattern. +You can generate `HttpRequest` instance with `finish()` method or you can +run your handler with `run()` or `run_async()` methods. + +```rust +# extern crate http; +# extern crate actix_web; +use http::{header, StatusCode}; +use actix_web::*; +use actix_web::test::TestRequest; + +fn index(req: HttpRequest) -> HttpResponse { + if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { + if let Ok(s) = hdr.to_str() { + return httpcodes::HTTPOk.response() + } + } + httpcodes::HTTPBadRequest.response() +} + +fn main() { + let resp = TestRequest::with_header("content-type", "text/plain") + .run(index) + .unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let resp = TestRequest::default() + .run(index) + .unwrap(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); +} +``` + + ## Integration tests There are several methods how you can test your application. Actix provides diff --git a/src/application.rs b/src/application.rs index 80b8173c..1de0ff5b 100644 --- a/src/application.rs +++ b/src/application.rs @@ -313,6 +313,7 @@ mod tests { use std::str::FromStr; use http::{Method, Version, Uri, HeaderMap, StatusCode}; use super::*; + use test::TestRequest; use httprequest::HttpRequest; use httpcodes; @@ -322,9 +323,7 @@ mod tests { .resource("/test", |r| r.h(httpcodes::HTTPOk)) .finish(); - let req = HttpRequest::new( - Method::GET, Uri::from_str("/test").unwrap(), - Version::HTTP_11, HeaderMap::new(), None); + let req = TestRequest::with_uri("/test").finish(); let resp = app.run(req); assert_eq!(resp.as_response().unwrap().status(), StatusCode::OK); diff --git a/src/handler.rs b/src/handler.rs index a5e34223..deab468e 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -412,6 +412,7 @@ impl Handler for NormalizePath { mod tests { use super::*; use http::{header, Method}; + use test::TestRequest; use application::Application; fn index(_req: HttpRequest) -> HttpResponse { @@ -438,7 +439,7 @@ mod tests { ("/resource2/?p1=1&p2=2", "", StatusCode::OK) ]; for (path, target, code) in params { - let req = app.prepare_request(HttpRequest::from_path(path)); + let req = app.prepare_request(TestRequest::with_uri(path).finish()); let resp = app.run(req); let r = resp.as_response().unwrap(); assert_eq!(r.status(), code); @@ -470,7 +471,7 @@ mod tests { ("/resource2/?p1=1&p2=2", StatusCode::OK) ]; for (path, code) in params { - let req = app.prepare_request(HttpRequest::from_path(path)); + let req = app.prepare_request(TestRequest::with_uri(path).finish()); let resp = app.run(req); let r = resp.as_response().unwrap(); assert_eq!(r.status(), code); @@ -501,7 +502,7 @@ mod tests { ("/////resource1/a//b/?p=1", "", StatusCode::NOT_FOUND), ]; for (path, target, code) in params { - let req = app.prepare_request(HttpRequest::from_path(path)); + let req = app.prepare_request(TestRequest::with_uri(path).finish()); let resp = app.run(req); let r = resp.as_response().unwrap(); assert_eq!(r.status(), code); @@ -558,7 +559,7 @@ mod tests { ("/////resource2/a///b/?p=1", "/resource2/a/b/?p=1", StatusCode::MOVED_PERMANENTLY), ]; for (path, target, code) in params { - let req = app.prepare_request(HttpRequest::from_path(path)); + let req = app.prepare_request(TestRequest::with_uri(path).finish()); let resp = app.run(req); let r = resp.as_response().unwrap(); assert_eq!(r.status(), code); diff --git a/src/httprequest.rs b/src/httprequest.rs index cf79ab33..ddaa4e98 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -119,31 +119,6 @@ impl HttpRequest<()> { HttpRequest(msg, None, None) } - /// Construct a new Request. - #[inline] - #[cfg(test)] - pub fn from_path(path: &str) -> HttpRequest - { - use std::str::FromStr; - - HttpRequest( - SharedHttpMessage::from_message(HttpMessage { - method: Method::GET, - uri: Uri::from_str(path).unwrap(), - version: Version::HTTP_11, - headers: HeaderMap::new(), - params: Params::default(), - cookies: None, - addr: None, - payload: None, - extensions: Extensions::new(), - info: None, - }), - None, - None, - ) - } - #[inline] /// Construct new http request with state. pub fn with_state(self, state: Rc, router: Router) -> HttpRequest { @@ -163,7 +138,7 @@ impl HttpRequest { // mutable reference should not be returned as result for request's method #[inline(always)] #[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref, inline_always))] - fn as_mut(&self) -> &mut HttpMessage { + pub(crate) fn as_mut(&self) -> &mut HttpMessage { self.0.get_mut() } @@ -657,30 +632,25 @@ mod tests { use std::str::FromStr; use router::Pattern; use resource::Resource; + use test::TestRequest; #[test] fn test_debug() { - let req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, HeaderMap::new(), None); + let req = TestRequest::with_header("content-type", "text/plain").finish(); let dbg = format!("{:?}", req); assert!(dbg.contains("HttpRequest")); } #[test] fn test_no_request_cookies() { - let req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, HeaderMap::new(), None); + let req = HttpRequest::default(); assert!(req.cookies().unwrap().is_empty()); } #[test] fn test_request_cookies() { - let mut headers = HeaderMap::new(); - headers.insert(header::COOKIE, - header::HeaderValue::from_static("cookie1=value1; cookie2=value2")); - - let req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, headers, None); + let req = TestRequest::with_header( + header::COOKIE, "cookie1=value1; cookie2=value2").finish(); { let cookies = req.cookies().unwrap(); assert_eq!(cookies.len(), 2); @@ -733,8 +703,7 @@ mod tests { #[test] fn test_request_match_info() { - let mut req = HttpRequest::new(Method::GET, Uri::from_str("/value/?id=test").unwrap(), - Version::HTTP_11, HeaderMap::new(), None); + let mut req = TestRequest::with_uri("/value/?id=test").finish(); let mut resource = Resource::<()>::default(); resource.name("index"); @@ -748,15 +717,10 @@ mod tests { #[test] fn test_chunked() { - let req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, HeaderMap::new(), None); + let req = HttpRequest::default(); assert!(!req.chunked().unwrap()); - let mut headers = HeaderMap::new(); - headers.insert(header::TRANSFER_ENCODING, - header::HeaderValue::from_static("chunked")); - let req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, headers, None); + let req = TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish(); assert!(req.chunked().unwrap()); let mut headers = HeaderMap::new(); diff --git a/src/test/mod.rs b/src/test/mod.rs index 247753b9..0d3c596f 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,17 +1,30 @@ //! Various helpers for Actix applications to use during testing. use std::{net, thread}; +use std::rc::Rc; use std::sync::mpsc; +use std::str::FromStr; +use std::collections::HashMap; use actix::{Arbiter, SyncAddress, System, msgs}; +use cookie::Cookie; +use http::{Uri, Method, Version, HeaderMap, HttpTryFrom}; +use http::header::{HeaderName, HeaderValue}; +use futures::Future; use tokio_core::net::TcpListener; +use tokio_core::reactor::Core; +use error::Error; use server::HttpServer; -use handler::Handler; +use handler::{Handler, Responder, ReplyItem}; use channel::{HttpHandler, IntoHttpHandler}; use middlewares::Middleware; use application::{Application, HttpApplication}; - +use param::Params; +use router::Router; +use payload::Payload; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; /// The `TestServer` type. /// @@ -192,3 +205,188 @@ impl Iterator for TestApp { } } } + +/// Test `HttpRequest` builder +/// +/// ```rust +/// # extern crate http; +/// # extern crate actix_web; +/// # use http::{header, StatusCode}; +/// # use actix_web::*; +/// use actix_web::test::TestRequest; +/// +/// fn index(req: HttpRequest) -> HttpResponse { +/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { +/// httpcodes::HTTPOk.response() +/// } else { +/// httpcodes::HTTPBadRequest.response() +/// } +/// } +/// +/// fn main() { +/// let resp = TestRequest::with_header("content-type", "text/plain") +/// .run(index).unwrap(); +/// assert_eq!(resp.status(), StatusCode::OK); +/// +/// let resp = TestRequest::default() +/// .run(index).unwrap(); +/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); +/// } +/// ``` +pub struct TestRequest { + state: S, + version: Version, + method: Method, + uri: Uri, + headers: HeaderMap, + params: Params<'static>, + cookies: Option>>, + payload: Option, +} + +impl Default for TestRequest<()> { + + fn default() -> TestRequest<()> { + TestRequest { + state: (), + method: Method::GET, + uri: Uri::from_str("/").unwrap(), + version: Version::HTTP_11, + headers: HeaderMap::new(), + params: Params::default(), + cookies: None, + payload: None, + } + } +} + +impl TestRequest<()> { + + /// Create TestReqeust and set request uri + pub fn with_uri(path: &str) -> TestRequest<()> { + TestRequest::default().uri(path) + } + + /// Create TestReqeust and set header + pub fn with_header(key: K, value: V) -> TestRequest<()> + where HeaderName: HttpTryFrom, + HeaderValue: HttpTryFrom + { + TestRequest::default().header(key, value) + } +} + +impl TestRequest { + + /// Start HttpRequest build process with application state + pub fn with_state(state: S) -> TestRequest { + TestRequest { + state: state, + method: Method::GET, + uri: Uri::from_str("/").unwrap(), + version: Version::HTTP_11, + headers: HeaderMap::new(), + params: Params::default(), + cookies: None, + payload: None, + } + } + + /// Set HTTP version of this request + pub fn version(mut self, ver: Version) -> Self { + self.version = ver; + self + } + + /// Set HTTP method of this request + pub fn method(mut self, meth: Method) -> Self { + self.method = meth; + self + } + + /// Set HTTP Uri of this request + pub fn uri(mut self, path: &str) -> Self { + self.uri = Uri::from_str(path).unwrap(); + self + } + + /// Set a header + pub fn header(mut self, key: K, value: V) -> Self + where HeaderName: HttpTryFrom, + HeaderValue: HttpTryFrom + { + if let Ok(key) = HeaderName::try_from(key) { + if let Ok(value) = HeaderValue::try_from(value) { + self.headers.append(key, value); + return self + } + } + panic!("Can not create header"); + } + + /// Set request path pattern parameter + pub fn param(mut self, name: &'static str, value: &'static str) -> Self { + self.params.add(name, value); + self + } + + /// Complete request creation and generate `HttpRequest` instance + pub fn finish(self) -> HttpRequest { + let TestRequest { state, method, uri, version, headers, params, cookies, payload } = self; + let req = HttpRequest::new(method, uri, version, headers, payload); + req.as_mut().cookies = cookies; + req.as_mut().params = params; + let (router, _) = Router::new::("/", HashMap::new()); + req.with_state(Rc::new(state), router) + } + + /// This method generates `HttpRequest` instance and runs handler + /// with generated request. + /// + /// This method panics is handler returns actor or async result. + pub fn run>(self, mut h: H) -> + Result>::Result as Responder>::Error> + { + let req = self.finish(); + let resp = h.handle(req.clone()); + + match resp.respond_to(req.clone_without_state()) { + Ok(resp) => { + match resp.into().into() { + ReplyItem::Message(resp) => Ok(resp), + ReplyItem::Actor(_) => panic!("Actor handler is not supported."), + ReplyItem::Future(_) => panic!("Async handler is not supported."), + } + }, + Err(err) => Err(err), + } + } + + /// This method generates `HttpRequest` instance and runs handler + /// with generated request. + /// + /// This method panics is handler returns actor. + pub fn run_async(self, h: H) -> Result + where H: Fn(HttpRequest) -> F + 'static, + F: Future + 'static, + R: Responder + 'static, + E: Into + 'static + { + let req = self.finish(); + let fut = h(req.clone()); + + let mut core = Core::new().unwrap(); + match core.run(fut) { + Ok(r) => { + match r.respond_to(req.clone_without_state()) { + Ok(reply) => match reply.into().into() { + ReplyItem::Message(resp) => Ok(resp), + _ => panic!("Nested async replies are not supported"), + }, + Err(e) => Err(e), + } + }, + Err(err) => Err(err), + } + } +}