How to unit-test a deserialization function used in serde(deserialize_with)? - unit-testing

I have a struct which implements Deserialize and uses the serde(deserialize_with) on a field:
#[derive(Debug, Deserialize)]
struct Record {
name: String,
#[serde(deserialize_with = "deserialize_numeric_bool")]
is_active: bool,
}
The implementation of deserialize_numeric_bool deserializes a string "0" or "1" to the corresponding boolean value:
pub fn deserialize_numeric_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
where D: Deserializer<'de>
{
struct NumericBoolVisitor;
impl<'de> Visitor<'de> for NumericBoolVisitor {
type Value = bool;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("either 0 or 1")
}
fn visit_u64<E>(self, value: u64) -> Result<bool, E>
where E: DeserializeError
{
match value {
0 => Ok(false),
1 => Ok(true),
_ => Err(E::custom(format!("invalid bool: {}", value))),
}
}
}
deserializer.deserialize_u64(NumericBoolVisitor)
}
(I appreciate comments about code improvements)
I'd like to write unit tests for deserialization functions like deserialize_numeric_bool. Of course, my friendly search box revealed the serde_test crate and a documentation page about unit-testing.
But these resources couldn't help me in my case, as the crate tests a structure directly implementing Deserialize.
One idea I had was to create a newtype which only contains the output of my deserialize functions and test it with it. But this looks like a unnecessary indirection to me.
#[derive(Deserialize)]
NumericBool {
#[serde(deserialize_with = "deserialize_numeric_bool")]
value: bool
};
How do I write idiomatic tests for it?

My current solution uses only structures already provided by serde.
In my use case, I only wanted to test that a given string will deserialize successfully into a bool or has a certain error. The serde::de::value provides simple deserializers for fundamental data types, for example U64Deserializer which holds a u64. It also has an Error struct which provides a minimal representation for the Error traits – ready to be used for mocking errors.
My tests look currently like that: I mock the input with a deserializer and pass it to my function under test. I like that I don't need an indirection there and that I have no additional dependencies. It is not as nice as the assert_tokens* provided serde_test, as it needs the error struct and feels less polished. But for my case, where only a single value is deserialized, it fulfills my needs.
use serde::de::IntoDeserializer;
use serde::de::value::{U64Deserializer, StrDeserializer, Error as ValueError};
#[test]
fn test_numeric_true() {
let deserializer: U64Deserializer<ValueError> = 1u64.into_deserializer();
assert_eq!(numeric_bool(deserializer), Ok(true));
}
#[test]
fn test_numeric_false() {
let deserializer: U64Deserializer<ValueError> = 0u64.into_deserializer();
assert_eq!(numeric_bool(deserializer), Ok(false));
}
#[test]
fn test_numeric_invalid_number() {
let deserializer: U64Deserializer<ValueError> = 2u64.into_deserializer();
let error = numeric_bool(deserializer).unwrap_err();
assert_eq!(error.description(), "invalid bool: 2");
}
#[test]
fn test_numeric_empty() {
let deserializer: StrDeserializer<ValueError> = "".into_deserializer();
let error = numeric_bool(deserializer).unwrap_err();
assert_eq!(error.description(), "invalid type: string \"\", expected either 0 or 1");
}
I hope that it helps other folks too or inspire other people to find a more polished version.

I've come across this question several times while trying to solve a similar problem recently. For future readers, pixunil's answer is nice, straightforward, and works well. However, I'd like to provide a solution using serde_test as the unit testing documentation mentions.
I researched how serde_test is used across a few crates that I found via its reverse dependencies on lib.rs. Several of them define small structs or enums for testing deserialization or serialization as you mentioned in your original post. I suppose doing so is idiomatic when testing would be too verbose otherwise.
Here's a few examples; this is a non-exhaustive list:
Example from time
Another example from time
Example from slab (tokio)
Example from bitcoin_hashes
Example from uuid
Example from euclid
Anyway, let's say I have a function to deserialize a bool from a u8 and another function that serializes a bool to a u8.
use serde::{
de::{Error as DeError, Unexpected},
Deserialize, Deserializer, Serialize, Serializer,
};
fn bool_from_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
match u8::deserialize(deserializer)? {
0 => Ok(false),
1 => Ok(true),
wrong => Err(DeError::invalid_value(
Unexpected::Unsigned(wrong.into()),
&"zero or one",
)),
}
}
#[inline]
fn bool_to_int<S>(a_bool: &bool, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if *a_bool {
serializer.serialize_u8(1)
} else {
serializer.serialize_u8(0)
}
}
I can test those functions by defining a struct in my test module. This allows constraining the tests to those functions specifically instead of ser/deserializing a larger object.
#[cfg(test)]
mod tests {
use super::{bool_from_int, bool_to_int};
use serde::{Deserialize, Serialize};
use serde_test::{assert_de_tokens_error, assert_tokens, Token};
#[derive(Debug, PartialEq, Deserialize, Serialize)]
#[serde(transparent)]
struct BoolTest {
#[serde(deserialize_with = "bool_from_int", serialize_with = "bool_to_int")]
a_bool: bool,
}
const TEST_TRUE: BoolTest = BoolTest { a_bool: true };
const TEST_FALSE: BoolTest = BoolTest { a_bool: false };
#[test]
fn test_true() {
assert_tokens(&TEST_TRUE, &[Token::U8(1)])
}
#[test]
fn test_false() {
assert_tokens(&TEST_FALSE, &[Token::U8(0)])
}
#[test]
fn test_de_error() {
assert_de_tokens_error::<BoolTest>(
&[Token::U8(14)],
"invalid value: integer `14`, expected zero or one",
)
}
}
BoolTest is within the tests module which is gated by #[cfg(test)] as per usual. This means that BoolTest is only compiled for tests rather than adding bloat. I'm not a Rust expert, but I think this is a good alternative if a programmer wishes to use serde_test as a harness.

Related

How to stub an external crate (or ways around it) in rust

I'm trying to test a struct I have that looks something like this
struct CANProxy {
socket: CANSocket
// other stuff .......
}
impl CANProxy {
pub fn new(can_device: &str) -> Self {
let socket = CANSocket::open(can_device).unwrap();
// other stuff .......
Self { socket }
}
}
What I want to test is that the proper messages are being sent across the socket, but I don't want to actually initialize a new can device while running my tests. I wanted to make a dummy CANSocket (which is from the cansocket crate) that uses the same functions and whatnot.
I tried creating a trait and extending the socketcan::CANSocket but it is super tedious and very redundant. I've looked at the mockall crate but I'm not sure if this would help in this situation. Is there an elegant way to accomplish what I want?
trait CANInterface {
fn open(name: &str) -> Result<Self, SomeError>;
// ... all the functions that are a part of the socketcan::CANSocket
// which is a lot of repetition
}
///////////// Proxy code
struct<T: CANInterface> CANProxy<T> {
socket: T
// other stuff .......
}
impl<T: CANInterface> CANProxy<T> {
pub fn open(can_device: &str) -> Result<Self, SomeError> {
let socket = T::open(can_device).unwrap();
// other stuff .......
Ok(Self { socket })
}
}
////////////// Stubbed CANInterfaces
struct FakeCANSocket;
impl CANInterface for FakeCANSocket {
// ..... implementing the trait here
}
// extension trait over here
impl CANInterface for socketcan::CANSocket {
// this is a lot of repetition and is kind of silly
// because I'm just calling the same things
fn open(can_device: &str) -> Self {
CANSocket::open(can_device)
}
/// ..............
/// ..............
/// ..............
}
So, first of all, there are indeed mock-targeted helper tools and crates such as ::mockall to help with these patterns, but only when you already have a trait-based API. If you don't, that part can be quite tedious.
For what is worth, know that there are also other helper crates to help write that boiler-plate-y and redundantly-delegating trait impls such as your open -> open situation. One such example could be the ::delegate crate.
Mocking it with a test-target Cargo feature
With all that being said, my personal take for your very specific situation —the objective is to override a genuine impl with a mock one, but just for testing purposes—, would be to forgo the structured but heavyweight approach of generics & traits, and to instead embrace "duck-typed" APIs, much like it is often done when having implementations on different platforms. In other words, the following suggestion, conceptually, could be interpreted as your test environment being one such special "platform".
You'd then #[cfg(…)]-feature-gate the usage of the real impl, that is, the CANSocket type, in one case, and #[cfg(not(…))]-feature gate a mock definition of your own CANSocket type, provided you managed to copy / mock all of the genuine's type API that you may, yourself, be using.
Add a mock-socket Cargo feature to your project:
[features]
mock-socket = []
Remark: some of you may be thinking of using cfg(test) rather than cfg(feature = "…"), but that approach only works for unit (src/… files with #[cfg(test)] mod tests, cargo test --lib invocation) tests, it doesn't for integration tests (tests/….rs files, cargo test --tests invocation) or doctests (cargo test --doc invocation), since the library itself is then compiled without cfg(test).
Then you can feature-gate Rust code using it
#[cfg(not(feature = "mock-socket"))]
use …path::to::genuine::CANSocket;
#[cfg(feature("mock-socket"))]
use my_own_mock_socket::CANSocket;
So that you can then define that my_own_mock_socket module (e.g., in a my_own_mock_socket.rs file using mod my_own_mock_socket; declaration), provided you don't forget to feature-gate it itself, so that the compiler doesn't waste time and effort compiling it when not using the mocked CANSocket (which would yield dead_code warnings and so on):
#[cfg(feature = "mock-socket")]
mod my_own_mock_socket {
//! It is important that you mimic the names and APIs of the genuine type!
pub struct CANSocket…
impl CANSocket { // <- no traits!
pub fn open(can_device: &str) -> Result<Self, SomeError> {
/* your mock logic */
}
…
}
}
That way, you can use:
either cargo test
or cargo test --features mock-socket
to run pick the implementation of your choice when running your tests
(Optional) if you know you will never want to run the tests for the real implementation, and only the mock one, then you may want to have that feature be enabled by default when running tests. While there is no direct way to achieve this, there is a creative way to work around it, by explicitly telling of the self-as-a-lib dev-dependency that test code has (this dependency is always present implicitly, for what is worth). By making it explicit, we can then use the classic features .toml attribute to enable features for that dev-dependency:
[dev-dependencies]
your_crate_name = { path = ".", features = ["mock-socket"] }
Bonus: not having to define an extra module for the mock code.
When the mock impls in question are short enough, it could be more tempting to just inline its definition and impl blocks. The issue then is that for every item so defined, it has to carry that #[cfg…] attribute which is annoying. That's when helper macros such as that of https://docs.rs/cfg-if can be useful, albeit adding a dependency for such a simple macro may seem a bit overkill (and, very personally, I find cfg_if!'s syntax too sigil heavy).
You can, instead, reimplement it yourself in less than a dozen lines of code:
macro_rules! cfg_match {
( _ => { $($tt:tt)* } $(,)? ) => ( $($tt)* );
( $cfg:meta => $expansion:tt $(, $($($rest:tt)+)?)? ) => (
#[cfg($cfg)]
cfg_match! { _ => $expansion }
$($(
#[cfg(not($cfg))]
cfg_match! { $($rest)+ }
)?)?
);
} use cfg_match;
With it, you can rewrite steps 2. and 3. above as:
cfg_match! {
feature = "mock-socket" => {
/// Mock implementation
struct CANSocket …
impl CANSocket { // <- no traits!
pub fn open(can_device: &str) -> Result<Self, SomeError> {
/* your mock logic */
}
…
}
},
_ => {
use …path::to::genuine::CANSocket;
},
}
You can avoid a lot of the boilerplate by using a macro to create the wrapper trait and implement it for the base struct. Simplified example:
macro_rules! make_wrapper {
($s:ty : $t:ident { $(fn $f:ident ($($p:ident $(: $pt:ty)?),*) -> $r:ty;)* }) => {
trait $t {
$(fn $f ($($p $(: $pt)?),*) -> $r;)*
}
impl $t for $s {
$(fn $f ($($p $(: $pt)?),*) -> $r { <$s>::$f ($($p),*) })*
}
}
}
struct TestStruct {}
impl TestStruct {
fn foo (self) {}
}
make_wrapper!{
TestStruct: TestTrait {
fn foo (self) -> ();
}
}
Playground
This will need to be extended to handle references (at least &self arguments), but you get the idea. You can refer to The Little Book of Rust Macros for more information on writing the macro.
Then you can use a crate like mockall to create your mock implementation of TestTrait or roll your own.

Unit testing, mocking and traits in rust [duplicate]

This question already has answers here:
How to mock external dependencies in tests? [duplicate]
(1 answer)
How to mock specific methods but not all of them in Rust?
(2 answers)
How can I test stdin and stdout?
(1 answer)
Is there a way of detecting whether code is being called from tests in Rust?
(1 answer)
What is the proper way to use the `cfg!` macro to choose between multiple implementations?
(1 answer)
Closed 2 years ago.
Iam current building a application which heavy relies on File IO, so obviously lots of parts of my code have File::open(file).
Doing some integration tests are ok, I can easily set folders to load file and scenarios needed for it.
The problem comes whatever I want to unit tests, and code branches. I know there is lots of mocking libraries out there that claim to mocks, but i feel my biggest problem is code design itself.
Let's say for instance, I would do the same code in any object oriented language (java in the example), i could write some interfaces, and on tests simple override the default behavior I want to mock, set the a fake ClientRepository, whatever reimplemented wih a fixed return, or use some mocking framework, like mockito.
public interface ClientRepository {
Client getClient(int id)
}
public class ClientRepositoryDB {
private ClientRepository repository;
//getters and setters
public Client getClientById(int id) {
Client client = repository.getClient(id);
//Some data manipulation and validation
}
}
But i couldn`t manage to get the same results in rust, since we endup mixing data with behavior.
On the RefCell documentation, there is a similar example with the one I gave on java. Some of answers points to traits, clojures, conditional compiliation
We might come with some scenarios in test, first one a public function in some mod.rs
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SomeData {
pub name: Option<String>,
pub address: Option<String>,
}
pub fn get_some_data(file_path: PathBuf) -> Option<SomeData> {
let mut contents = String::new();
match File::open(file_path) {
Ok(mut file) => {
match file.read_to_string(&mut contents) {
Ok(result) => result,
Err(_err) => panic!(
panic!("Problem reading file")
),
};
}
Err(err) => panic!("File not find"),
}
// using serde for operate on data output
let some_data: SomeData = match serde_json::from_str(&contents) {
Ok(some_data) => some_data,
Err(err) => panic!(
"An error occour when parsing: {:?}",
err
),
};
//we might do some checks or whatever here
Some(some_data) or None
}
mod test {
use super::*;
#[test]
fn test_if_scenario_a_happen() -> std::io::Result<()> {
//tied with File::open
let some_data = get_some_data(PathBuf::new);
assert!(result.is_some());
Ok(())
}
#[test]
fn test_if_scenario_b_happen() -> std::io::Result<()> {
//We might need to write two files, and we want to test is the logic, not the file loading itself
let some_data = get_some_data(PathBuf::new);
assert!(result.is_none());
Ok(())
}
}
The second the same function becoming a trait and some struct implement it.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SomeData {
pub name: Option<String>,
pub address: Option<String>,
}
trait GetSomeData {
fn get_some_data(&self, file_path: PathBuf) -> Option<SomeData>;
}
pub struct SomeDataService {}
impl GetSomeData for SomeDataService {
fn get_some_data(&self, file_path: PathBuf) -> Option<SomeData> {
let mut contents = String::new();
match File::open(file_path) {
Ok(mut file) => {
match file.read_to_string(&mut contents) {
Ok(result) => result,
Err(_err) => panic!("Problem reading file"),
};
}
Err(err) => panic!("File not find"),
}
// using serde for operate on data output
let some_data: SomeData = match serde_json::from_str(&contents) {
Ok(some_data) => some_data,
Err(err) => panic!("An error occour when parsing: {:?}", err),
};
//we might do some checks or whatever here
Some(some_data) or None
}
}
impl SomeDataService {
pub fn do_something_with_data(&self) -> Option<SomeData> {
self.get_some_data(PathBuf::new())
}
}
mod test {
use super::*;
#[test]
fn test_if_scenario_a_happen() -> std::io::Result<()> {
//tied with File::open
let service = SomeDataService{}
let some_data = service.do_something_with_data(PathBuf::new);
assert!(result.is_some());
Ok(())
}
}
On both examples, we have a hard time unit testing it, since we tied with File::open, and surely, this might be extend to any non-deterministic function, like time, db connection, etc.
How would you design this or any similar code to make easier to unit testing and better design?
How would you design this or any similar code to make easier to unit testing and better design?
One way is to make get_some_data() generic over the input stream. The std::io module defines a Read trait for all things you can read from, so it could look like this (untested):
use std::io::Read;
pub fn get_some_data(mut input: impl Read) -> Option<SomeData> {
let mut contents = String::new();
input.read_to_string(&mut contents).unwrap();
...
}
You'd call get_some_data() with the input, e.g. get_some_data(File::open(file_name).unwrap()) or get_some_data(&mut io::stdin::lock()), etc. When testing, you can prepare the input in a string and call it as get_some_data(io::Cursor::new(prepared_data)).
As for the trait example, I think you misunderstood how to apply the pattern to your code. You're supposed to use the trait to decouple getting the data from processing the data, sort of how you'd use an interface in Java. The get_some_data() function would receive an object known to implement the trait.
Code more similar to what you'd find in an OO language might choose to use a trait object:
trait ProvideData {
fn get_data(&self) -> String
}
struct FileData(PathBuf);
impl ProvideData for FileData {
fn get_data(&self) -> String {
std::fs::read(self.0).unwrap()
}
}
pub fn get_some_data(data_provider: &dyn ProvideData) -> Option<SomeData> {
let contents = data_provider.get_data();
...
}
// normal invocation:
// let some_data = get_some_data(&FileData("file name".into()));
In test you'd just create a different implementation of the trait - for example:
#[cfg(test)]
mod test {
struct StaticData(&'static str);
impl ProvideData for StaticData {
fn get_data(&self) -> String {
self.0.to_string()
}
}
#[test]
fn test_something() {
let some_data = get_some_data(StaticData("foo bar"));
assert!(...);
}
}
First of all, I would like to thank #user4815162342 for enlightenment of traits. Using his answer as base, i solve with my own solution for the problem.
First, I build as mention, traits to better design my code:
trait ProvideData {
fn get_data(&self) -> String
}
But I had some problems, since there were tons of bad design code, and lots code I had to mock before run the test, something like the below code.
pub fn some_function() -> Result<()> {
let some_data1 = some_non_deterministic_function(PathBuf::new())?;
let some_data2 = some_non_deterministic_function_2(some_data1);
match some_data2 {
Ok(ok) => Ok(()),
Err(err) => panic!("something went wrong"),
}
}
I would need to change almost all functions signatures to accept Fn, this would not only change most my code, but will actually make it hard to read, since most of it I was changing for testing purpose only.
pub fn some_function(func1: Box<dyn ProvideData>, func2: Box<dyn SomeOtherFunction>) -> Result<()> {
let some_data1 = func1(PathBuf::new())?;
let some_data2 = func2(some_data1);
match some_data2 {
Ok(ok) => Ok(()),
Err(err) => panic!("something went wrong"),
}
}
Reading a little more deep the rust documentation, I slight changed the implementation.
Change almost all my code to use traits and structs ( Lots of code were public functions )
trait ProvideData {
fn get_data(&self) -> String;
}
struct FileData(PathBuf);
impl ProvideData for FileData {
fn get_data(&self) -> String {
String::from(format!("Pretend there is something going on here with file {}", self.0.to_path_buf().display()))
}
}
Add a new functions for default implementation in the structs, and add constructor with default implementation using dynamic dispatch functions.
struct SomeData(Box<dyn ProvideData>);
impl SomeData {
pub fn new() -> SomeData {
let file_data = FileData(PathBuf::new());
SomeData {
0: Box::new(file_data)
}
}
pub fn get_some_data(&self) -> Option<String> {
let contents = self.0.get_data();
Some(contents)
}
}
Since the constructor is private, we prevent user from injecting code, and we can freely change the internal implementation for testing purpose, and the integration tests keep running smooth.
fn main() {
//When the user call this function, it would no know that there is multiple implementations for it.
let some_data = SomeData::new();
assert_eq!(Some(String::from("Pretend there is something going on here with file ")),some_data.get_some_data());
println!("HEY WE CHANGE THE INJECT WITHOUT USER INTERATION");
}
And finally, since we test inside the declaration scope, we might change the injection even if is private:
mod test {
use super::*;
struct MockProvider();
impl ProvideData for MockProvider {
fn get_data(&self) -> String {
String::from("Mocked data")
}
}
#[test]
fn test_internal_data() {
let some_data = SomeData(Box::from(MockProvider()));
assert_eq!(Some(String::from("Mocked data")), some_data.get_some_data())
}
#[test]
fn test_ne_internal_data() {
let some_data = SomeData(Box::from(MockProvider()));
assert_ne!(Some(String::from("Not the expected data")), some_data.get_some_data())
}
}
The result code can be seem in the rust playground, hope this help user to design their code.
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=62348977502accfed55fa4600d149bcd

Making a mock implementation record any number of arguments

Context & my attempts
I am trying to unit-test a function, and I want to make sure it calls the correct API endpoint with the correct arguments.
My first idea was to make the function I wanted to test accept the API as a trait object. This would allow me to mock it. I also had to pass mutable references, because the mock should record information about how it was called. Below you see a simplified representation of my code structure:
trait API {
fn func_a(&mut self, arg1: i32, arg2: String);
}
// prod trait implementation
#[allow(dead_code)]
fn business_logic(api: &mut dyn API) {
api.func_a(42, String::from("mytrousersareblue"));
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct Mock<A, B> {
args: (A, B),
}
impl API for Mock<i32, String> {
fn func_a(&mut self, arg1: i32, arg2: String) {
self.args = (arg1, arg2);
}
}
#[test]
fn business_logic_performs_correct_calls() {
let mut mock: Mock<i32, String> = Default::default();
business_logic(&mut mock);
assert_eq!(mock.args.0, 42);
assert_eq!(mock.args.1, String::from("mytrousersareblue"));
}
}
This has various downsides, mainly that I have to make separate Mock structs for functions with zero, one, two, three, and more parameters.
So another idea that came to my mind was to implement the Mock struct as follows:
struct Mock<'a> {
args: Vec<&'a dyn Eq>,
}
This would allow me to record as many arguments as I want. However the compiler complains that the trait std::cmp::Eq cannot be made into an object.
I cannot use std::any::Any, because I still need to be able to determine equality in the assert_eq statements.
Maybe I am taking a completely wrong path here. I do have quite some trouble with the Rust mindset, coming from a Java background...
Summary / TLDR
I want to have a mock-implementation of the API, which records all arguments it was called with, that can later be compared against expected values.
If I'm correct, then I'll need a way to make a struct store any number of objects, which all extend at least Eq. However, if there's a nicer solution: I'm open.
I've found two solutions:
The practical one:
Use the mockall crate (thanks #Richard Matheson for the recommendation). Seriously, don't try to re-implement mocking yourself. The mockall crates does it using macros, and it is pure magic. But it works.
The nightmare:
I don't recommend using this answer, but it is helpful to know as a design concept.
The way to solve this problem, is to declare a trait implementing Any and defining the following functions:
trait Arg: Any {
fn as_any(&self) -> &dyn Any;
fn equals_arg(&self, _: &dyn Any) -> bool;
}
Then you implement this trait for as many structs as possible:
impl<S: 'static + PartialEq> Arg for S {
fn as_any(&self) -> &dyn Any {
self
}
fn equals_arg(&self, other: &dyn Arg) -> bool {
other
.as_any()
.downcast_ref::<S>()
.map_or(false, |a| self == a)
}
}
Implementing the as_any function is straightforward. The equals_arg function is trickier. We compare these two references by first trying to downcast the other object to the type of self. If that fails, we immediately return false. If it succeeds, we compare them using the == operator, made available because both implement the PartialEq trait.
We can now store an array of dyn Arg elements and compare them using equals_arg. See the playground for an example implementation. (In the playground I changed the API interface to accept &'static types. This could certainly be improved, but I am a beginner concerning lifetimes.)
References
https://dev.to/magnusstrale/rust-trait-objects-in-a-vector-non-trivial-4co5
How to test for equality between trait objects?

Is there a simple way to conditionally enable or ignore entire test suites in Rust?

I’m working on a Rust library that provides access to some hardware devices. There are two device types, 1 and 2, and the functionality for type 2 is a superset of the functionality for type 1.
I want to provide different test suites for different circumstances:
tests with no connected device (basic sanity checks, e. g. for CI servers)
tests for the shared functionality (requires a device of type 1 or 2)
tests for the type 2 exclusive functionality (requires a device of type 2)
I’m using features to represent this behavior: a default feature test-no-device and optional features test-type-one and test-type-two. Then I use the cfg_attr attribute to ignore the tests based on the selected features:
#[test]
#[cfg_attr(not(feature = "test-type-two"), ignore)]
fn test_exclusive() {
// ...
}
#[test]
#[cfg_attr(not(any(feature = "test-type-two", feature = "test-type-one")), ignore)]
fn test_shared() {
// ...
}
This is rather cumbersome as I have to duplicate this condition for every test and the conditions are hard to read and maintain.
Is there any simpler way to manage the test suites?
I tried to set the ignore attribute when declaring the module, but apparently it can only be set for each test function. I think I could disable compilation of the excluded tests by using cfg on the module, but as the tests should always compile, I would like to avoid that.
Is there a simple way to conditionally enable or ignore entire test suites in Rust?
The easiest is to not even compile the tests:
#[cfg(test)]
mod test {
#[test]
fn no_device_needed() {}
#[cfg(feature = "test1")]
mod test1 {
fn device_one_needed() {}
}
#[cfg(feature = "test2")]
mod test2 {
fn device_two_needed() {}
}
}
I have to duplicate this condition for every test and the conditions are hard to read and maintain.
Can you represent the desired functionality in pure Rust? yes
Is the existing syntax overly verbose? yes
This is a candidate for a macro.
macro_rules! device_test {
(no-device, $name:ident, {$($body:tt)+}) => (
#[test]
fn $name() {
$($body)+
}
);
(device1, $name:ident, {$($body:tt)+}) => (
#[test]
#[cfg_attr(not(feature = "test-type-one"), ignore)]
fn $name() {
$($body)+
}
);
(device2, $name:ident, {$($body:tt)+}) => (
#[test]
#[cfg_attr(not(feature = "test-type-two"), ignore)]
fn $name() {
$($body)+
}
);
}
device_test!(no-device, one, {
assert_eq!(2, 1+1)
});
device_test!(device1, two, {
assert_eq!(3, 1+1)
});
the functionality for type 2 is a superset of the functionality for type 1
Reflect that in your feature definitions to simplify the code:
[features]
test1 = []
test2 = ["test1"]
If you do this, you shouldn't need to have any or all in your config attributes.
a default feature test-no-device
This doesn't seem useful; instead use normal tests guarded by the normal test config:
#[cfg(test)]
mod test {
#[test]
fn no_device_needed() {}
}
If you follow this, you can remove this case from the macro.
I think if you follow both suggestions, you don't even need the macro.

Creating an `std::env::Args` iterator for testing

Is there a way in Rust to create a std::env::Args from a Vec<String> in order to use it in a #[test] function?
I wish to test a function that gets a std::env::Args as an argument, but I don't know how to create such an object with a list of arguments I supply for the test.
I wasn't able to figure this one out from the docs, the source nor from Google searches.
The fields of std::env::Args are not documented, and there doesn't appear to be a public function to create one with custom fields. So, you're outta luck there.
But since it's just "An iterator over the arguments of a process, yielding a String value for each argument" your functions can take a String iterator or Vec without any loss of functionality or type safety. Since it's just a list of Strings, it doesn't make much sense to arbitrarily limit your functions to strings which happen to come from the command line.
Looking through Rust's own tests, that's just what they do. There's a lot of let args: Vec<String> = env::args().collect();
There's even an example in rustbuild where they strip off the name of the program and just feed the list of arguments.
use std::env;
use bootstrap::{Config, Build};
fn main() {
let args = env::args().skip(1).collect::<Vec<_>>();
let config = Config::parse(&args);
Build::new(config).build();
}
And bootstrap::Config::parse() looks like so:
impl Config {
pub fn parse(args: &[String]) -> Config {
let flags = Flags::parse(&args);
...
I'm not a Rust expert, but that seems to be how the Rust folks handle the problem.
#Schwern's answer is good and it led me to this simpler version. Since std::env::Args implements Iterator with Item = String you can do this:
use std::env;
fn parse<T>(args: T)
where
T: Iterator<Item = String>,
{
for arg in args {
// arg: String
print!("{}", arg);
}
}
fn main() {
parse(env::args());
}
To test, you provide parse with an iterator over String:
#[test]
fn test_parse() {
let args = ["arg1", "arg2"].iter().map(|s| s.to_string());
parse(args);
}
I've wrote a little macro to make this easier, based on #Rossman's answer (and therefore also based on #Schwern's answer; thanks go to both):
macro_rules! make_string_iter {
($($element: expr), *) => {
{
let mut v = Vec::new();
$( v.push(String::from($element)); )*
v.into_iter()
}
};
}
It can be used in that way:
macro_rules! make_string_iter {
($($element: expr), *) => {
{
let mut v = Vec::new();
$( v.push(String::from($element)); )*
v.into_iter()
}
};
}
// We're using this function to test our macro
fn print_args<T: Iterator<Item = String>>(args: T) {
for item in args {
println!("{}", item);
}
}
fn main() {
// Prints a, b and c
print_args(make_string_iter!("a", "b", "c"))
}
Or try it out on the Rust Playground.
I'm not (yet) an expert in rust, any suggestions are highly welcome :)