1
0
Fork 0
mirror of https://github.com/actix/actix-web.git synced 2024-10-21 09:23:54 +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
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

View file

@ -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);

View file

@ -412,6 +412,7 @@ impl<S> Handler<S> 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);

View file

@ -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<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
#[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();

View file

@ -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<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),
}
}
}