Skip to content

Commit

Permalink
Implement mockito (#6)
Browse files Browse the repository at this point in the history
* Create class skeleton for a mock server

* Try to return data to the TcpStream

* First passing test

* Move MockError to error mod

* Small cleanup to start to make Server real

* Small cleanup

* Use SocketAddr

* Pipe the state to the request handler

* Start to flesh out the Mock

* Start of the match function

* A bit more mock work

* Make tests pass in a more real way

* Add mock module

* Add state module

* Add server module

* Run cargo fmt

* Connect mockito to the feature flag

* Add feature around deps mod
  • Loading branch information
tyleragreen authored Sep 15, 2023
1 parent b70193d commit f98fdda
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 4 deletions.
7 changes: 3 additions & 4 deletions example-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,18 @@ tulsa = { path = "../tulsa" }
axum = "0.6.18"
hyper = { version = "0.14.27", features = ["client"] }
prost = "0.12"
rand = "0.8.5"
reqwest = { version = "0.11.18", features = ["json", "blocking"] }
serde = { version = "1.0.171", features = ["derive"] }
serde_json = "1.0.103"
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] }
tower = "0.4.13"
ureq = "2.7.1"
mime = { version = "0.3.17", optional = true }
mockito = { version = "1.1.0", optional = true }

[build-dependencies]
prost-build = "0.12"

[dev-dependencies]
mockito = "1.1.0"

[features]
use_dependencies = ["mime"]
use_dependencies = [ "mime", "mockito" ]
15 changes: 15 additions & 0 deletions example-app/src/deps/mockito/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use std::error::Error;
use std::fmt;

#[derive(Debug)]
pub struct MockError {
message: String,
}

impl fmt::Display for MockError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "MockError: {}", self.message)
}
}

impl Error for MockError {}
91 changes: 91 additions & 0 deletions example-app/src/deps/mockito/mock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use hyper::StatusCode;
use hyper::{Body, Request};
use rand;
use std::sync::{Arc, RwLock};

use super::state::State;

#[derive(Clone)]
pub struct Response {
pub status: StatusCode,
pub body: Vec<u8>,
}

impl Default for Response {
fn default() -> Self {
Response {
status: StatusCode::OK,
body: vec![],
}
}
}

#[derive(Clone)]
pub struct InnerMock {
pub id: usize,
pub method: String,
pub path: String,
pub response: Response,
pub num_called: usize,
}

#[derive(Clone)]
pub struct Mock {
pub state: Arc<RwLock<State>>,
pub inner: InnerMock,
}

impl Mock {
pub fn new(state: Arc<RwLock<State>>, method: &str, path: &str) -> Mock {
Mock {
state,
inner: InnerMock {
id: rand::random(),
method: method.to_owned().to_uppercase(),
path: path.to_owned(),
response: Response::default(),
num_called: 0,
},
}
}

pub fn with_status(mut self, status: u16) -> Mock {
self.inner.response.status = StatusCode::from_u16(status).unwrap();
self
}

pub fn with_body(mut self, body: Vec<u8>) -> Mock {
self.inner.response.body = body;
self
}

pub fn create(self) -> Mock {
let state = self.state.clone();
let mut state = state.write().unwrap();
state.mocks.push(self.clone());
self
}

pub fn assert(&self) {
let state = self.state.clone();
let state = state.read().unwrap();
let num_called = state
.mocks
.iter()
.find(|mock| mock.inner.id == self.inner.id)
.unwrap()
.inner
.num_called;

if num_called == 0 {
panic!("Mock not called");
}
}

pub fn matches(&self, request: &Request<Body>) -> bool {
let method = request.method().to_string();
let path = request.uri().path().to_string();

method == self.inner.method && path == self.inner.path
}
}
6 changes: 6 additions & 0 deletions example-app/src/deps/mockito/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mod error;
mod mock;
mod server;
mod state;

pub use server::Server;
85 changes: 85 additions & 0 deletions example-app/src/deps/mockito/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use hyper::server::conn::Http;
use hyper::service::service_fn;
use hyper::{Body, Request, Response as HyperResponse};
use std::net::SocketAddr;
use std::sync::{Arc, RwLock};
use std::thread;
use tokio::net::TcpListener;
use tokio::runtime;
use tokio::task::spawn;

use super::error::MockError;
use super::mock::Mock;
use super::state::State;

pub struct Server {
address: SocketAddr,
state: Arc<RwLock<State>>,
}

impl Server {
pub fn new() -> Server {
let address = SocketAddr::from(([127, 0, 0, 1], 5001));
let state = Arc::new(RwLock::new(State::new()));

let runtime = runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();

let state_b = state.clone();
thread::spawn(move || {
runtime.block_on(async {
let listener = TcpListener::bind(address).await.unwrap();

while let Ok((stream, _)) = listener.accept().await {
let state_c = state_b.clone();
spawn(async move {
let _ = Http::new()
.serve_connection(
stream,
service_fn(move |request: Request<Body>| {
handle_request(request, state_c.clone())
}),
)
.await;
});
}
});
});

Server { address, state }
}

pub fn mock(&self, method: &str, path: &str) -> Mock {
Mock::new(self.state.clone(), method, path)
}

pub fn url(&self) -> String {
format!("http://{}", self.address.to_string())
}
}

async fn handle_request(
request: Request<Body>,
state: Arc<RwLock<State>>,
) -> Result<HyperResponse<Body>, MockError> {
let state_b = state.clone();
let mut state = state_b.write().unwrap();
let mut matching: Vec<&mut Mock> = vec![];

for mock in state.mocks.iter_mut() {
if mock.matches(&request) {
matching.push(mock);
}
}
let mock = matching.first_mut();

if let Some(mock) = mock {
mock.inner.num_called += 1;
let response = HyperResponse::new(Body::from(mock.inner.response.body.clone()));
Ok(response)
} else {
panic!("No matching mock found");
}
}
11 changes: 11 additions & 0 deletions example-app/src/deps/mockito/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use super::mock::Mock;

pub struct State {
pub mocks: Vec<Mock>,
}

impl State {
pub fn new() -> Self {
State { mocks: vec![] }
}
}
3 changes: 3 additions & 0 deletions example-app/src/fetcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ pub async fn recurring_fetch(feed: Feed) {

#[cfg(test)]
mod tests {
#[cfg(not(feature = "use_dependencies"))]
use crate::deps::mockito;

use super::*;
use std::collections::HashMap;
use std::fs;
Expand Down
3 changes: 3 additions & 0 deletions example-app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ pub mod scheduler_interface;
// a learning exercise. I do not plan to make this code public and my
// implementations will definitely be similar to the original since I am not
// yet fluent in Rust.
#[cfg(not(feature = "use_dependencies"))]
pub mod deps {
// mime = "0.3.17" -> https://docs.rs/mime/0.3.17/mime/
pub mod mime;
// mockito = "1.1.0" -> https://docs.rs/mockito/1.1.0/mockito/
pub mod mockito;
}

0 comments on commit f98fdda

Please sign in to comment.