1
0
Fork 0
mirror of https://github.com/actix/actix-web.git synced 2024-10-21 17:33:59 +00:00

add unit test helper

This commit is contained in:
Nikolay Kim 2017-12-26 19:48:02 -08:00
parent 7f77ba557d
commit 743235b8fd
5 changed files with 253 additions and 54 deletions

View file

@ -3,6 +3,43 @@
Every application should be well tested and. Actix provides the tools to perform unit and Every application should be well tested and. Actix provides the tools to perform unit and
integration tests. 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 ## Integration tests
There are several methods how you can test your application. Actix provides There are several methods how you can test your application. Actix provides

View file

@ -313,6 +313,7 @@ mod tests {
use std::str::FromStr; use std::str::FromStr;
use http::{Method, Version, Uri, HeaderMap, StatusCode}; use http::{Method, Version, Uri, HeaderMap, StatusCode};
use super::*; use super::*;
use test::TestRequest;
use httprequest::HttpRequest; use httprequest::HttpRequest;
use httpcodes; use httpcodes;
@ -322,9 +323,7 @@ mod tests {
.resource("/test", |r| r.h(httpcodes::HTTPOk)) .resource("/test", |r| r.h(httpcodes::HTTPOk))
.finish(); .finish();
let req = HttpRequest::new( let req = TestRequest::with_uri("/test").finish();
Method::GET, Uri::from_str("/test").unwrap(),
Version::HTTP_11, HeaderMap::new(), None);
let resp = app.run(req); let resp = app.run(req);
assert_eq!(resp.as_response().unwrap().status(), StatusCode::OK); assert_eq!(resp.as_response().unwrap().status(), StatusCode::OK);

View file

@ -412,6 +412,7 @@ impl<S> Handler<S> for NormalizePath {
mod tests { mod tests {
use super::*; use super::*;
use http::{header, Method}; use http::{header, Method};
use test::TestRequest;
use application::Application; use application::Application;
fn index(_req: HttpRequest) -> HttpResponse { fn index(_req: HttpRequest) -> HttpResponse {
@ -438,7 +439,7 @@ mod tests {
("/resource2/?p1=1&p2=2", "", StatusCode::OK) ("/resource2/?p1=1&p2=2", "", StatusCode::OK)
]; ];
for (path, target, code) in params { 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 resp = app.run(req);
let r = resp.as_response().unwrap(); let r = resp.as_response().unwrap();
assert_eq!(r.status(), code); assert_eq!(r.status(), code);
@ -470,7 +471,7 @@ mod tests {
("/resource2/?p1=1&p2=2", StatusCode::OK) ("/resource2/?p1=1&p2=2", StatusCode::OK)
]; ];
for (path, code) in params { 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 resp = app.run(req);
let r = resp.as_response().unwrap(); let r = resp.as_response().unwrap();
assert_eq!(r.status(), code); assert_eq!(r.status(), code);
@ -501,7 +502,7 @@ mod tests {
("/////resource1/a//b/?p=1", "", StatusCode::NOT_FOUND), ("/////resource1/a//b/?p=1", "", StatusCode::NOT_FOUND),
]; ];
for (path, target, code) in params { 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 resp = app.run(req);
let r = resp.as_response().unwrap(); let r = resp.as_response().unwrap();
assert_eq!(r.status(), code); assert_eq!(r.status(), code);
@ -558,7 +559,7 @@ mod tests {
("/////resource2/a///b/?p=1", "/resource2/a/b/?p=1", StatusCode::MOVED_PERMANENTLY), ("/////resource2/a///b/?p=1", "/resource2/a/b/?p=1", StatusCode::MOVED_PERMANENTLY),
]; ];
for (path, target, code) in params { 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 resp = app.run(req);
let r = resp.as_response().unwrap(); let r = resp.as_response().unwrap();
assert_eq!(r.status(), code); assert_eq!(r.status(), code);

View file

@ -119,31 +119,6 @@ impl HttpRequest<()> {
HttpRequest(msg, None, None) 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] #[inline]
/// Construct new http request with state. /// Construct new http request with state.
pub fn with_state<S>(self, state: Rc<S>, router: Router) -> HttpRequest<S> { pub fn with_state<S>(self, state: Rc<S>, router: Router) -> HttpRequest<S> {
@ -163,7 +138,7 @@ impl<S> HttpRequest<S> {
// mutable reference should not be returned as result for request's method // mutable reference should not be returned as result for request's method
#[inline(always)] #[inline(always)]
#[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref, 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() self.0.get_mut()
} }
@ -657,30 +632,25 @@ mod tests {
use std::str::FromStr; use std::str::FromStr;
use router::Pattern; use router::Pattern;
use resource::Resource; use resource::Resource;
use test::TestRequest;
#[test] #[test]
fn test_debug() { fn test_debug() {
let req = HttpRequest::new( let req = TestRequest::with_header("content-type", "text/plain").finish();
Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, HeaderMap::new(), None);
let dbg = format!("{:?}", req); let dbg = format!("{:?}", req);
assert!(dbg.contains("HttpRequest")); assert!(dbg.contains("HttpRequest"));
} }
#[test] #[test]
fn test_no_request_cookies() { fn test_no_request_cookies() {
let req = HttpRequest::new( let req = HttpRequest::default();
Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, HeaderMap::new(), None);
assert!(req.cookies().unwrap().is_empty()); assert!(req.cookies().unwrap().is_empty());
} }
#[test] #[test]
fn test_request_cookies() { fn test_request_cookies() {
let mut headers = HeaderMap::new(); let req = TestRequest::with_header(
headers.insert(header::COOKIE, header::COOKIE, "cookie1=value1; cookie2=value2").finish();
header::HeaderValue::from_static("cookie1=value1; cookie2=value2"));
let req = HttpRequest::new(
Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, headers, None);
{ {
let cookies = req.cookies().unwrap(); let cookies = req.cookies().unwrap();
assert_eq!(cookies.len(), 2); assert_eq!(cookies.len(), 2);
@ -733,8 +703,7 @@ mod tests {
#[test] #[test]
fn test_request_match_info() { fn test_request_match_info() {
let mut req = HttpRequest::new(Method::GET, Uri::from_str("/value/?id=test").unwrap(), let mut req = TestRequest::with_uri("/value/?id=test").finish();
Version::HTTP_11, HeaderMap::new(), None);
let mut resource = Resource::<()>::default(); let mut resource = Resource::<()>::default();
resource.name("index"); resource.name("index");
@ -748,15 +717,10 @@ mod tests {
#[test] #[test]
fn test_chunked() { fn test_chunked() {
let req = HttpRequest::new( let req = HttpRequest::default();
Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, HeaderMap::new(), None);
assert!(!req.chunked().unwrap()); assert!(!req.chunked().unwrap());
let mut headers = HeaderMap::new(); let req = TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish();
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);
assert!(req.chunked().unwrap()); assert!(req.chunked().unwrap());
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();

View file

@ -1,17 +1,30 @@
//! Various helpers for Actix applications to use during testing. //! Various helpers for Actix applications to use during testing.
use std::{net, thread}; use std::{net, thread};
use std::rc::Rc;
use std::sync::mpsc; use std::sync::mpsc;
use std::str::FromStr;
use std::collections::HashMap;
use actix::{Arbiter, SyncAddress, System, msgs}; 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::net::TcpListener;
use tokio_core::reactor::Core;
use error::Error;
use server::HttpServer; use server::HttpServer;
use handler::Handler; use handler::{Handler, Responder, ReplyItem};
use channel::{HttpHandler, IntoHttpHandler}; use channel::{HttpHandler, IntoHttpHandler};
use middlewares::Middleware; use middlewares::Middleware;
use application::{Application, HttpApplication}; use application::{Application, HttpApplication};
use param::Params;
use router::Router;
use payload::Payload;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
/// The `TestServer` type. /// The `TestServer` type.
/// ///
@ -192,3 +205,188 @@ impl<S: 'static> Iterator for TestApp<S> {
} }
} }
} }
/// 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<S> {
state: S,
version: Version,
method: Method,
uri: Uri,
headers: HeaderMap,
params: Params<'static>,
cookies: Option<Vec<Cookie<'static>>>,
payload: Option<Payload>,
}
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<K, V>(key: K, value: V) -> TestRequest<()>
where HeaderName: HttpTryFrom<K>,
HeaderValue: HttpTryFrom<V>
{
TestRequest::default().header(key, value)
}
}
impl<S> TestRequest<S> {
/// Start HttpRequest build process with application state
pub fn with_state(state: S) -> TestRequest<S> {
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<K, V>(mut self, key: K, value: V) -> Self
where HeaderName: HttpTryFrom<K>,
HeaderValue: HttpTryFrom<V>
{
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<S> {
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::<S>("/", 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<H: Handler<S>>(self, mut h: H) ->
Result<HttpResponse, <<H as Handler<S>>::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<H, R, F, E>(self, h: H) -> Result<HttpResponse, E>
where H: Fn(HttpRequest<S>) -> F + 'static,
F: Future<Item=R, Error=E> + 'static,
R: Responder<Error=E> + 'static,
E: Into<Error> + '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),
}
}
}