mirror of
https://github.com/fafhrd91/actix-web
synced 2024-11-24 08:22:59 +01:00
add unit test helper
This commit is contained in:
parent
7f77ba557d
commit
743235b8fd
@ -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
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
202
src/test/mod.rs
202
src/test/mod.rs
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user