├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── lib.rs └── macro_helper.rs └── tests └── test.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .idea 4 | .vscode 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mock_me" 3 | version = "0.2.3" 4 | authors = ["David Raifaizen "] 5 | description = "MockMe is a tool used to mock dependencies / function calls when running unit (lib) tests in Rust." 6 | repository = "https://github.com/craftytrickster/mock_me" 7 | readme = "README.md" 8 | license = "MIT" 9 | 10 | [dependencies] 11 | mock_me_test_context = "0.1.2" 12 | 13 | [lib] 14 | crate-type = ["proc-macro"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David Raifaizen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MockMe 2 | ====== 3 | 4 | MockMe is a tool used to mock dependencies / function calls when running unit (lib) tests in Rust. 5 | 6 | ## Caution 7 | 8 | Unfortunately, due to the non determinstic order of how tests are performed by rust/cargo, 9 | a mocked function can only be injected by a single test. If it is mocked by multiple tests, 10 | it may be subject to random panics due to race conditions. 11 | As a result, this is mainly just an experimental toy crate and it should not be used in 12 | production codebases. 13 | 14 | 15 | ## How to Use 16 | 17 | *Currently, only works on nightly.* 18 | 19 | Simply use the macro as seen in the example below. 20 | When this code is run normally, MockMe will have no effect. 21 | However, when the code is run as part of a unit test `#[cfg(test)]`, 22 | the mocked token will be used instead. 23 | 24 | ```rust 25 | 26 | #![feature(proc_macro)] 27 | extern crate mockme; 28 | use mockme::{mock, inject}; 29 | 30 | // Below we will create two mocking identifiers called id_1 and id_2. 31 | // We will then provide the name of the two functions we are mocking, as well as 32 | // their type signature. In future iterations, hopefully the signature won't be needed. 33 | #[mock(id_1="external_db_call: fn(u32) -> String", id_2="other_call: fn() -> String")] 34 | fn my_super_cool_function() -> String { 35 | let input = 42u32; 36 | // external_db_call will be replaced with fake_mocked_call during testing 37 | let db_result = external_db_call(input); 38 | 39 | // other_call will also be replaced 40 | let other_result = other_call(); 41 | format!("I have two results! {} and {}", db_result, other_result) 42 | } 43 | 44 | // Finally, when we run our tests, we simply need to provide the identifier we previously used, 45 | // as well as the name of the replacement function 46 | #[test] 47 | #[inject(id_1="db_fake", id_2="other_fake")] 48 | fn actual_test2() { 49 | let result = my_super_cool_function(); 50 | assert_eq!(result, "I have two results! Faker! and This is indeed a disturbing universe."); 51 | } 52 | 53 | fn db_fake(_: u32) -> String { "Faker!".to_string() } 54 | fn other_fake() -> String { "This is indeed a disturbing universe.".to_string() } 55 | ``` 56 | 57 | ## Contributions 58 | 59 | All contributions are welcome! This library is still in its infancy, so everything helps. 60 | Code contributions, feature requests and bug reports are all appreciated. 61 | 62 | ## Limitations 63 | 64 | Currently, the library is unable to infer the signature of the function that is being mocked. As a result, 65 | the programmer needs to provide it, which hurts the ergonomics of the library. 66 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! MockMe is a tool used to mock dependencies / function calls when running unit (lib) tests in Rust. 2 | //! 3 | //! ## How to Use 4 | //! 5 | //! Simply use the macro as seen in the example below. 6 | //! When this code is run normally, MockMe will have no effect. 7 | //! However, when the code is run as part of a unit test #[cfg(test)], 8 | //! the mocked token will be used instead. 9 | //! 10 | //! In order to use, we first must mark the functions that we would like to mock. 11 | //! When running tests, we then identify the replacement function that we would like to inject. 12 | //! 13 | //! ```rust,ignore 14 | //! 15 | //! #![feature(proc_macro)] 16 | //! extern crate mock_me; 17 | //! use mock_me::{mock, inject}; 18 | //! 19 | //! // Below we will create two mocking identifiers called id_1 and id_2. 20 | //! // We will then provide the name of the two functions we are mocking, as well as 21 | //! // their type signature. In future iterations, hopefully the signature won't be needed. 22 | //! #[mock(id_1="external_db_call: fn(u32) -> String", id_2="other_call: fn() -> String")] 23 | //! fn my_super_cool_function() -> String { 24 | //! let input = 42u32; 25 | //! // external_db_call will be replaced with fake_mocked_call during testing 26 | //! let db_result = external_db_call(input); 27 | //! 28 | //! // other_call will also be replaced 29 | //! let other_result = other_call(); 30 | //! format!("I have two results! {} and {}", db_result, other_result) 31 | //! } 32 | //! 33 | //! // Finally, when we run our tests, we simply need to provide the identifier we previously used, 34 | //! // as well as the name of the replacement function 35 | //! #[test] 36 | //! #[inject(id_1="db_fake", id_2="other_fake")] 37 | //! fn actual_test2() { 38 | //! let result = my_super_cool_function(); 39 | //! assert_eq!(result, "I have two results! Faker! and This is indeed a disturbing universe."); 40 | //! } 41 | //! 42 | //! fn db_fake(_: u32) -> String { "Faker!".to_string() } 43 | //! fn other_fake() -> String { "This is indeed a disturbing universe.".to_string() } 44 | //! 45 | //! ``` 46 | 47 | #![feature(proc_macro)] 48 | extern crate proc_macro; 49 | use proc_macro::TokenStream; 50 | 51 | use std::fmt::Write; 52 | 53 | mod macro_helper; 54 | use macro_helper::*; 55 | 56 | /// The mock macro is used mock a concrete function that is not desired during unit tests. 57 | /// Its signature contains the identifier that is being mocked, with the function that will replace 58 | /// the mocked function within quotes, as well as the mocked function signature. 59 | #[proc_macro_attribute] 60 | pub fn mock(attr: TokenStream, item: TokenStream) -> TokenStream { 61 | let mock_matches = get_mock_matches(&*attr.to_string()); 62 | 63 | let mut source = item.to_string(); 64 | 65 | // I should find a more structured way of injecting test context into top of method 66 | let insertion_point = source.find("{").unwrap() + 1; 67 | source.insert_str(insertion_point, HEADER); 68 | 69 | let mut modified_source = source.clone(); 70 | for m_match in mock_matches { 71 | let ctx_getter = format!(r#" 72 | (unsafe {{ 73 | let _mock_me_test_usize_func = _mock_me_test_context_instance.get("{}"); 74 | let _mock_me_test_transmuted_func: {} = std::mem::transmute(_mock_me_test_usize_func); 75 | _mock_me_test_transmuted_func 76 | }}) 77 | "#, m_match.identifier, m_match.function_signature); 78 | 79 | // string replacement should be more controlled ideally than a blind replace 80 | modified_source = modified_source.replace(&*m_match.function_to_mock, &*ctx_getter); 81 | } 82 | 83 | 84 | let branched_source = format!( 85 | r#" 86 | #[cfg(not(test))] 87 | {} 88 | 89 | #[cfg(test)] 90 | {} 91 | "#, source, modified_source 92 | ); 93 | 94 | 95 | branched_source.parse().unwrap() 96 | } 97 | 98 | /// The inject macro is used to replace a mocked function with an alternative implementation. 99 | /// Its signature contains the identifier that is being mocked, with the function that will replace 100 | /// the mocked function within quotes. 101 | #[proc_macro_attribute] 102 | pub fn inject(attr: TokenStream, item: TokenStream) -> TokenStream { 103 | let inject_matches = get_inject_matches(&*attr.to_string()); 104 | 105 | let mut source = item.to_string(); 106 | 107 | let mut context_setter_string = HEADER.to_string(); 108 | 109 | for i_match in inject_matches { 110 | write!( 111 | context_setter_string, 112 | "_mock_me_test_context_instance.set(\"{}\".to_string(), {} as usize);\n", 113 | i_match.identifier, i_match.function_to_mock 114 | ).unwrap(); 115 | } 116 | 117 | // I should find a more structured way of injecting test context into top of method 118 | let insertion_point = source.find("{").unwrap() + 1; 119 | source.insert_str(insertion_point, &*context_setter_string); 120 | 121 | source.parse().unwrap() 122 | } -------------------------------------------------------------------------------- /src/macro_helper.rs: -------------------------------------------------------------------------------- 1 | pub const HEADER: &'static str = r#" 2 | extern crate mock_me_test_context; 3 | let _mock_me_test_context_instance = mock_me_test_context::get_test_context(); 4 | "#; 5 | 6 | #[derive(Debug, PartialEq)] 7 | pub struct InjectMatch { 8 | pub identifier: String, 9 | pub function_to_mock: String, 10 | } 11 | 12 | #[derive(Debug, PartialEq)] 13 | pub struct MockMatch { 14 | pub identifier: String, 15 | pub function_to_mock: String, 16 | pub function_signature: String 17 | } 18 | 19 | pub fn get_inject_matches(attr_str: &str) -> Vec { 20 | let p = Parser::new(attr_str); 21 | p.get_inject_matches() 22 | } 23 | 24 | pub fn get_mock_matches(attr_str: &str) -> Vec { 25 | let p = Parser::new(attr_str); 26 | p.get_mock_matches() 27 | } 28 | 29 | #[cfg(test)] 30 | mod test { 31 | use super::*; 32 | 33 | #[test] 34 | fn inject_macro_values_should_parse_correctly() { 35 | let token_string = r#"( id_1 = "db_fake" , id_2 = "other_fake" )"#; 36 | 37 | let inject_matches = get_inject_matches(token_string); 38 | assert_eq!(inject_matches, vec![ 39 | InjectMatch { identifier: "id_1".to_string(), function_to_mock: "db_fake".to_string() }, 40 | InjectMatch { identifier: "id_2".to_string(), function_to_mock: "other_fake".to_string() }, 41 | ]); 42 | } 43 | 44 | #[test] 45 | fn mock_macro_values_should_parse_correctly() { 46 | let token_string = r#"( 47 | id_1 = "external_db_call: fn(u32) -> String" , id_2 = 48 | "other_call: fn() -> String" )"#; 49 | 50 | let mock_matches = get_mock_matches(token_string); 51 | assert_eq!(mock_matches, vec![ 52 | MockMatch { 53 | identifier: "id_1".to_string(), 54 | function_to_mock: "external_db_call".to_string(), 55 | function_signature: "fn(u32) -> String".to_string() 56 | }, 57 | MockMatch{ 58 | identifier: "id_2".to_string(), 59 | function_to_mock: "other_call".to_string(), 60 | function_signature: "fn() -> String".to_string() 61 | } 62 | ]); 63 | } 64 | } 65 | 66 | 67 | // inspired in https://limpet.net/mbrubeck/2014/08/11/toy-layout-engine-2.html 68 | struct Parser<'a> { 69 | pos: usize, 70 | input: &'a str 71 | } 72 | 73 | impl<'a> Parser<'a> { 74 | fn new(input: &'a str) -> Self { 75 | Parser { pos: 0, input: input } 76 | } 77 | 78 | fn get_inject_matches(mut self) -> Vec { 79 | self.execute(Parser::consume_inject_match) 80 | } 81 | 82 | fn get_mock_matches(mut self) -> Vec { 83 | self.execute(Parser::consume_mock_match) 84 | } 85 | 86 | fn execute(&mut self, mut consume_function: F) -> Vec where F: FnMut(&mut Self) -> R { 87 | let mut result = Vec::new(); 88 | 89 | assert_eq!(self.consume_char(), '('); 90 | 91 | loop { 92 | self.consume_whitespace(); 93 | 94 | if self.eof() || self.next_char() == ')' { 95 | break; 96 | } 97 | 98 | let item = consume_function(self); 99 | result.push(item); 100 | 101 | if !self.consume_has_separator() { 102 | break; 103 | } 104 | } 105 | 106 | self.consume_whitespace(); 107 | assert_eq!(self.consume_char(), ')'); 108 | result 109 | } 110 | 111 | fn consume_has_separator(&mut self) -> bool { 112 | self.consume_whitespace(); 113 | 114 | let mut has_next = false; 115 | if self.next_char() == ',' { 116 | has_next = true; 117 | self.consume_char(); 118 | } 119 | 120 | has_next 121 | } 122 | 123 | fn consume_inject_match(&mut self) -> InjectMatch { 124 | let identifier = self.parse_text(); 125 | 126 | self.consume_whitespace(); 127 | assert_eq!(self.consume_char(), '='); 128 | self.consume_whitespace(); 129 | 130 | assert_eq!(self.consume_char(), '"'); 131 | let function_to_mock = self.parse_text(); 132 | self.consume_whitespace(); 133 | assert_eq!(self.consume_char(), '"'); 134 | 135 | InjectMatch { identifier: identifier, function_to_mock: function_to_mock } 136 | } 137 | 138 | fn consume_mock_match(&mut self) -> MockMatch { 139 | let identifier = self.parse_text(); 140 | 141 | self.consume_whitespace(); 142 | assert_eq!(self.consume_char(), '='); 143 | self.consume_whitespace(); 144 | 145 | assert_eq!(self.consume_char(), '"'); 146 | let function_to_mock = self.parse_text(); 147 | self.consume_whitespace(); 148 | assert_eq!(self.consume_char(), ':'); 149 | self.consume_whitespace(); 150 | let function_signature = self.consume_while(|c| c != '"'); 151 | assert_eq!(self.consume_char(), '"'); 152 | 153 | MockMatch { 154 | identifier: identifier, 155 | function_to_mock: function_to_mock, 156 | function_signature: function_signature 157 | } 158 | } 159 | 160 | fn next_char(&self) -> char { 161 | self.input.as_bytes()[self.pos] as char 162 | } 163 | 164 | fn eof(&self) -> bool { 165 | self.pos >= self.input.len() 166 | } 167 | 168 | fn consume_char(&mut self) -> char { 169 | let cur_char = self.input.as_bytes()[self.pos]; 170 | self.pos += 1; 171 | cur_char as char 172 | } 173 | 174 | fn consume_while(&mut self, test: F) -> String where F: Fn(char) -> bool { 175 | let mut result = String::new(); 176 | 177 | while !self.eof() && test(self.next_char()) { 178 | result.push(self.consume_char()); 179 | } 180 | 181 | result 182 | } 183 | 184 | fn consume_whitespace(&mut self) { 185 | self.consume_while(char::is_whitespace); 186 | } 187 | 188 | fn parse_text(&mut self) -> String { 189 | self.consume_while(|c| c.is_alphanumeric() || c == '_') 190 | } 191 | } -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro)] 2 | extern crate mock_me; 3 | use mock_me::{mock, inject}; 4 | 5 | #[mock(id_1="external_db_call: fn(u32) -> String", id_2="other_call: fn() -> String")] 6 | fn my_super_cool_function() -> String { 7 | let input = 42u32; 8 | // external_db_call will be replaced with fake_mocked_call during testing 9 | let db_result = external_db_call(input); 10 | 11 | // other_call will also be replaced 12 | let other_result = other_call(); 13 | format!("I have two results! {} and {}", db_result, other_result) 14 | } 15 | 16 | #[test] 17 | #[inject(id_1="db_fake", id_2="other_fake")] 18 | fn actual_test2() { 19 | let result = my_super_cool_function(); 20 | assert_eq!(result, "I have two results! Faker! and This is indeed a disturbing universe."); 21 | } 22 | 23 | fn db_fake(_: u32) -> String { "Faker!".to_string() } 24 | fn other_fake() -> String { "This is indeed a disturbing universe.".to_string() } 25 | 26 | 27 | 28 | #[mock(fun="silly_func: fn(f64, f64) -> f64")] 29 | fn function_with_fun_id() -> f64 { 30 | silly_func(30f64, 20f64) 31 | } 32 | 33 | #[allow(dead_code)] 34 | fn silly_func(num_1: f64, num_2: f64) -> f64 { 35 | num_1 + num_2 36 | } 37 | 38 | #[test] 39 | #[inject(fun="replacement_1")] 40 | fn test_with_silly_func_1() { 41 | let result = function_with_fun_id(); 42 | assert_eq!(result, 10f64); 43 | } 44 | 45 | fn replacement_1(num_1: f64, num_2: f64) -> f64 { 46 | num_1 - num_2 47 | } 48 | --------------------------------------------------------------------------------