Macro use case – writing tests

Macros are used quite a lot when writing test cases for unit tests. Let's say you were writing a HTTP client library and you would like to test your client on various HTTP verbs such as GET or POST and on a variety of different URLs. The usual way you would write your tests is to create functions for each type of request and the URL. However, there's a better way to do this. Using macros, you can cut down your testing time by many folds by building a small DSL to perform the tests, which is readable and can also be type checked at compiled time. To demonstrate this, let's create a new crate by running cargo new http_tester --lib, which contains our macro definition. This macro implements a small language that's designed for describing simple HTTP GET/POST tests to a URL. Here's a sample of what the language looks like:

http://duckduckgo.com GET => 200
http://httpbin.org/post POST => 200, "key" => "value"

The first line makes a GET request to duckduckgo.com, and expects a return code of 200 (Status Ok). The second one makes a POST request to httpbin.org, along with form parameters "key"="value" with a custom syntax. It also expects a return code of 200. This is very simplistic but sufficient for demonstration purposes.

We'll assume that we already have our library implemented and will use a HTTP request library called reqwest. We'll add a dependency on reqwest in our Cargo.toml file:

# http_tester/Cargo.toml

[dependencies]
reqwest = "0.9.5"

Here's lib.rs:

// http_tester/src/lib.rs

#[macro_export]
macro_rules! http_test {
($url:tt GET => $code:expr) => {
let request = reqwest::get($url).unwrap();
println!("Testing GET {} => {}", $url, $code);
assert_eq!(request.status().as_u16(), $code);
};
($url:tt POST => $code:expr, $($k:expr => $v:expr),*) => {
let params = [$(($k, $v),)*];
let client = reqwest::Client::new();
let res = client.post($url)
.form(&params)
.send().unwrap();
println!("Testing POST {} => {}", $url, $code);
assert_eq!(res.status().as_u16(), $code);
};
}

#[cfg(test)]
mod tests {
#[test]
fn test_http_verbs() {
http_test!("http://duckduckgo.com" GET => 200);
http_test!("http://httpbin.org/post" POST => 200, "hello" => "world", "foo" => "bar");
}
}

Within the macro definition, we just match on the rules, which is where GET and POST are treated as literal tokens. Within the arms, we create our request client and assert on the status code that's returned by the input, which is provided to the macro. The POST test case also has a custom syntax for providing query parameters such as key => value, which is collected as an array in the params variable. This is then passed to the form method of the reqwest::post builder method. We'll explore the request library more when we get to Chapter 13, Building Web Applications in Rust.

Let's run cargo test and see the output:

running 1 test
test tests::test_http_verbs ... ok

Take a moment to think about what the benefit of using a macro here is. This could be implemented as a #[test] annotated function call as well, but the macro has a few benefits, even in this basic form. One benefit is that the HTTP verb is checked at compile time and our tests are now more declarative. If we try to invoke the macro with a test case that is not accounted for (say, HTTP DELETE), we'll get the following error:

error: no rules expected the token `DELETE`

Apart from using them for enumerating tests cases, macros are also used to generate Rust code based on some outside environmental state (such as database tables, time and date, and so on). They can be used to decorate structures with custom attributes, generating arbitrary code for them at compile time, or to create new linter plugins for making additional static analysis that the Rust compiler itself does not support. A great example is the clippy lint tool, which we've used already. Macros are also used to generate code that invokes native C libraries. We'll see how that happens when we get to Chapter 10, Unsafe Rust and Foreign Function Interfaces.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset