如何对serde(deserialize_with)中使用的反序列化函数进行单元测试?

17

我有一个结构体,实现了Deserialize并在一个字段上使用了serde(deserialize_with)

#[derive(Debug, Deserialize)]
struct Record {
    name: String,
    #[serde(deserialize_with = "deserialize_numeric_bool")]
    is_active: bool,
}

deserialize_numeric_bool 的实现将字符串 "0" 或 "1" 反序列化为相应的布尔值:

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)
}

(我感激有关代码改进的评论)

我想为反序列化函数编写单元测试,例如deserialize_numeric_bool。 当然,我的友好搜索框显示了serde_test包和有关单元测试的文档页面。 但是,在我的情况下,这些资源无法帮助我,因为该包是直接实现Deserialize的结构体的测试。

我想到的一个想法是创建一个新类型,其中仅包含我的反序列化函数的输出,并对其进行测试。 但对我来说,这似乎是不必要的间接操作。

#[derive(Deserialize)]
NumericBool {
    #[serde(deserialize_with = "deserialize_numeric_bool")]
    value: bool
};

我该如何编写符合惯用语的测试?


1
我对惯用的Serde知识不够了解,无法给出正确的答案,但你可以通过像 serde-json 这样的 crate 实例化一个 Deserializer 实现,然后将其传递到函数中。如果在测试中使用 JSON crate 感觉奇怪,你可以使用类似 serde-value 的东西。 - Joe Clay
你找到更直接的解决方案了吗? - zbrox
3个回答

4

我的当前解决方案仅使用serde已提供的结构。

在我的用例中,我只想测试给定的字符串是否能成功反序列化为布尔值或是否具有特定错误。 serde::de::value 提供了基本数据类型的简单反序列化器,例如包含u64U64Deserializer。它还具有一个Error 结构体,为错误特征提供了最小表示形式-准备好用于模拟错误。

我的测试目前看起来像这样:我使用反序列化器模拟输入,并将其传递给我的被测试函数。我喜欢它不需要额外的依赖项和间接性。虽然它需要错误结构体并且感觉不太完美,不如serde_test提供的assert_tokens*,但对于我的情况,只需反序列化单个值即可满足我的需求。

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");
}

我希望这也能帮助其他人,或者激励其他人寻找更加完善的版本。

2

serde_test 无法直接用于测试您的函数,因为 serde_test 没有公开其内部使用的 Deserializer。因此,serde_test 只能用于测试 SerializeDeserialize 实现,并且不能用于测试旨在与 deserialize_with 一起使用的函数。

但是,您可以使用 serde_assert crate(免责声明:我编写了 serde_assert)来实现这一点。 serde_assert 公开了一个 Deserializer,可直接用于测试,这使得直接测试您的函数非常简单:

use serde_assert::{de, Deserializer, Token, Tokens};

#[test]
fn test_numeric_true() {
    let mut deserializer = Deserializer::builder()
        .tokens(Tokens(vec![Token::U64(1)]))
        .build();

    assert_eq!(deserialize_numeric_bool(&mut deserializer), Ok(true),);
}

#[test]
fn test_numeric_false() {
    let mut deserializer = Deserializer::builder()
        .tokens(Tokens(vec![Token::U64(0)]))
        .build();

    assert_eq!(deserialize_numeric_bool(&mut deserializer), Ok(false),);
}

#[test]
fn test_numeric_invalid_value() {
    let mut deserializer = Deserializer::builder()
        .tokens(Tokens(vec![Token::U64(2)]))
        .build();

    assert_eq!(
        deserialize_numeric_bool(&mut deserializer),
        Err(de::Error::Custom("invalid bool: 2".to_owned())),
    );
}

#[test]
fn test_numeric_invalid_type() {
    let mut deserializer = Deserializer::builder()
        .tokens(Tokens(vec![Token::Str("".to_owned())]))
        .build();

    assert_eq!(
        deserialize_numeric_bool(&mut deserializer),
        Err(de::Error::InvalidType(
            "string \"\"".to_owned(),
            "either 0 or 1".to_owned()
        )),
    );
}

1
最近在解决类似问题时,我已经遇到了这个问题好几次。对于未来的读者,pixunil的答案很好,简单明了,而且效果很好。然而,我想提供一个使用serde_test作为单元测试文档中提到的解决方案。
我研究了一下通过lib.rs的反向依赖项找到的几个箱子中如何使用serde_test。其中有几个定义了小的结构体或枚举来测试反序列化或序列化,就像你在原始帖子中提到的那样。我认为,如果测试过程太冗长,这样做是符合惯例的。
以下是一些示例;这是一个非详尽列表: 无论如何,假设我有一个函数可以从一个 u8 中反序列化出一个 bool,并且另一个函数可以将一个 bool 序列化成一个 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)
    }
}

我可以通过在我的test模块中定义一个struct来测试这些函数。这样可以将测试限制在特定的函数上,而不是对更大的对象进行序列化/反序列化。

#[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",
        )
    }
}

BoolTesttests模块中,该模块通常由#[cfg(test)]进行门控。这意味着BoolTest仅编译用于测试而不会增加冗余代码。尽管我不是Rust专家,但我认为如果程序员想要使用serde_test作为测试工具,这是一个好的替代方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接