Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 607
pixunil Avatar asked Jul 11 '19 08:07

pixunil


1 Answers

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.

like image 144
pixunil Avatar answered Nov 09 '22 13:11

pixunil