I'm using Serde TOML crate (toml = "0.9.7") to serialize a struct that has a HashMap field. The problem is that the generated String randomly shuffles the HashMap elements and therefore my tests randomly fails. Could you please recommend a solution for testing?
Is it possible to use a BTreeMap instead? Even if you can't change what the type of the field is, you could still cause it to be serialized as a BTreeMap via a custom #[serde(serialize_with = "...")] converter.
I could change it, but it seems wrong to change types just for testing. Especially because BTreeMap is a more complicated (heavier?) type with functionality that I don't need.
Enable the preserve_order feature in toml in your dev-dependencies. Something like:
[dev-dependencies]
toml = { version = "0.9.7", default-features = false, features = ["preserve_order"] }
Other Serde-compatible crates may have something similar (e.g., serde_json has a feature of the same name that preserves the order). If you don't enable that, then you have no choice but to alter your unit/integration/doc tests to be order-agnostic.
You may have to implement the serialization manually as well since typically one doesn't care about the order of iteration of a HashMap. You can make such a custom implementation only for testing via #[cfg(test)] where you order the elements of the HashMap first (e.g., by converting it into a BTreeMap).
Without knowing more about the specifics, here is one way assuming you have enabled the preserve_order feature:
#[cfg(test)]
use serde::ser::{Serialize, Serializer};
#[cfg(test)]
use std::collections::{BTreeMap, HashMap};
#[cfg(test)]
struct Foo(HashMap<String, u8>);
#[cfg(test)]
impl Serialize for Foo {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0
.iter()
.collect::<BTreeMap<_, _>>()
.serialize(serializer)
}
}
#[cfg(test)]
mod tests {
use super::{Foo, HashMap};
use toml::ser::Error;
#[test]
fn toml() -> Result<(), Error> {
let mut map = HashMap::with_capacity(8);
for i in 0u8..8 {
_ = map.insert(i.to_string(), i);
}
assert_eq!(
"0 = 0\n1 = 1\n2 = 2\n3 = 3\n4 = 4\n5 = 5\n6 = 6\n7 = 7\n",
toml::to_string(&Foo(map))?
);
Ok(())
}
}
You don't want your non-tests to be adversely affected from the performance of transforming a HashMap into a BTreeMap nor do you want to bloat the compiled code in non-test environments, so you define a wrapper type Foo around HashMap that implements Serialize in a way that guarantees consistent order. This is all done under #[cfg(test)]. Obviously you'll have to adjust to your needs.
Indeed, but I assumed the OP wanted to keep using a HashMap. They specifically requested a solution for testing. Normally I don't change my code just for the sake of simpler testing.
Thank you. I solved it by a single test with two elements in HashMap and comparing two versions of the serialized string - not very intelligent solution but it works.
I understand that HashMap doesn't preserve order and that's OK, that's what I need. But it would be great to have a serialization option that would for example sort the keys alphabetically.