How to test TOML serialization for a HashMap?

Hi folks,

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?

Thank you.

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.

Thank you @jwodder for the reply.

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.

Do you want the TOML entries to have a stable ordering, or do you want your test to pass with any ordering?

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.

IndexMap can be used to get consistent order without the loss of performance of ‘BTreeMap` so long as the inputs have a consistent order.

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.

FYI, serde_test exists to test Serialize/Deserialize implementations regardless of the actual format used.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.