C#如何使用AutoFixture简化单元测试字符串参数

本文关键字:单元测试 字符串 参数 何使用 AutoFixture | 更新日期: 2023-09-27 18:26:53

我正在尝试创建一种简单的方法来测试单元测试中的字符串参数,对于大多数字符串参数,我想检查参数为Null、Empty或仅由空格组成时的行为。

在大多数情况下,我使用字符串检查参数。IsNullOrWhiteSpace(),如果它具有这三个值之一,则抛出异常。

现在,对于单元测试,我似乎必须为每个字符串参数编写三个单元测试。一个表示空值,一个表示空白值。

想象一个有3或4个字符串参数的方法,然后我需要写9或12个单元测试。。。

有人能想出一个简单的方法来测试这个吗?也许使用AutoFixture?

C#如何使用AutoFixture简化单元测试字符串参数

为了避免多次重复同一测试,可以编写参数化测试

如果你使用xUnit,你会写一个所谓的理论。一个理论意味着你正在证明一个原理,即当给定相同类型的不同样本时,某个函数的行为与预期一致。例如:

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Should_throw_argument_exception_when_input_string_is_invalid(string input)
{
    Assert.Throws<ArgumentException>(() => SystemUnderTest.SomeFunction(input));
}

xUnit运行程序将多次运行此测试,每次都将input参数分配给[InlineData]属性中指定的值之一。

如果正在测试的函数有多个参数,那么您可能不在乎将哪些值传递给其余的值,只要其中至少有一个是null、空或仅包含空白的字符串即可。

在这种情况下,可以将参数化测试与AutoFixture结合使用。AutoFixture旨在为您提供通用测试数据,当您不必关心确切值时,该数据足以在大多数情况下使用。

为了将其与xUnit理论一起使用,您需要将AutoFixture.Xuit-NuGet包(或AutoFixture.Xunit2,具体取决于您使用的版本)添加到您的测试项目中,并使用InlineAutoData属性:

[Theory]
[InlineAutoData(null)]
[InlineAutoData("")]
[InlineAutoData(" ")]
public void Should_throw_argument_exception_when_the_first_input_string_is_invalid(
    string input1,
    string input2,
    string input3)
{
    Assert.Throws<ArgumentException>(() =>
        SystemUnderTest.SomeFunction(input1, input2, input3));
}

只有input1参数将具有特定值,而其余参数将由AutoFixture指定为随机字符串。

这里需要注意的一点是,通过[InlineAutoData]属性传递的值是根据它们的位置分配给测试参数的。由于我们需要分别测试所有三个参数的相同行为,因此我们必须编写三个测试:

[Theory]
[InlineAutoData(null)]
[InlineAutoData("")]
[InlineAutoData(" ")]
public void Should_throw_argument_exception_when_the_second_input_string_is_invalid(
    string input2,
    string input1,
    string input3)
{
    Assert.Throws<ArgumentException>(() =>
        SystemUnderTest.SomeFunction(input1, input2, input3));
}
[Theory]
[InlineAutoData(null)]
[InlineAutoData("")]
[InlineAutoData(" ")]
public void Should_throw_argument_exception_when_the_third_input_string_is_invalid(
    string input3,
    string input1,
    string input2)
{
    Assert.Throws<ArgumentException>(() =>
        SystemUnderTest.SomeFunction(input1, input2, input3));
}

基于属性的测试

我忍不住认为,这类测试场景与基于属性的测试非常匹配。在不涉及太多细节的情况下,基于属性的测试是关于通过使用生成的输入多次运行来证明函数的特定行为(或"属性")

换句话说:

基于属性的测试生成关于代码输出的语句基于输入,并且这些语句被验证了许多不同的可能输入。

在您的情况下,只要至少有一个参数是null、空字符串或仅包含空白的字符串,就可以编写一个测试来验证函数是否抛出ArgumentException

在.NET中,可以使用名为FsCheck的库来编写和执行基于属性的测试。虽然API主要设计为从F#使用,但它也可以从C#使用。

然而,在这种特殊的情况下,我认为您最好坚持使用常规的参数化测试和AutoFixture来实现相同的目标。事实上,用FsCheck和C#编写这些测试最终会变得更加冗长,而且在健壮性方面不会给你带来太大好处。

更新:AutoFixture.习语

正如@RubenBartelink在评论中指出的那样,还有另一种选择。AutoFixture将常见断言封装在一个名为AutoFixture.Idioms的小型库中。您可以使用它来集中对方法如何验证string参数的期望,并在测试中使用它们。

虽然我对这种方法有保留意见,但为了完整起见,我将在这里添加它作为另一种可能的解决方案:

[Theory, AutoData]
public void Should_throw_argument_exception_when_the_input_strings_are_invalid(
    ValidatesTheStringArguments assertion)
{
    var sut = typeof(SystemUnderTest).GetMethod("SomeMethod");
    assertion.Verify(sut);
}
public class ValidatesTheStringArguments : GuardClauseAssertion
{
    public ValidatesTheStringArguments(ISpecimenBuilder builder)
        : base(
              builder,
              new CompositeBehaviorExpectation(
                  new NullReferenceBehaviorExpectation(),
                  new EmptyStringBehaviorExpectation(),
                  new WhitespaceOnlyStringBehaviorExpectation()))
    {
    }
}
public class EmptyStringBehaviorExpectation : IBehaviorExpectation
{
    public void Verify(IGuardClauseCommand command)
    {
        if (!command.RequestedType.IsClass
            && !command.RequestedType.IsInterface)
        {
            return;
        }
        try
        {
            command.Execute(string.Empty);
        }
        catch (ArgumentException)
        {
            return;
        }
        catch (Exception e)
        {
            throw command.CreateException("empty", e);
        }
        throw command.CreateException("empty");
    }
}
public class WhitespaceOnlyStringBehaviorExpectation : IBehaviorExpectation
{
    public void Verify(IGuardClauseCommand command)
    {
        if (!command.RequestedType.IsClass
            && !command.RequestedType.IsInterface)
        {
            return;
        }
        try
        {
            command.Execute(" ");
        }
        catch (ArgumentException)
        {
            return;
        }
        catch (Exception e)
        {
            throw command.CreateException("whitespace", e);
        }
        throw command.CreateException("whitespace");
    }
}

根据NullReferenceBehaviorExpectationEmptyStringBehaviorExpectationWhitespaceOnlyStringBehaviorExpectation中表达的期望,AutoFixture将自动尝试分别使用null、空字符串和空白来调用名为"SomeMethod"的方法。

如果方法没有抛出正确的异常(如预期类内的catch块中所指定的),则AutoFixture本身将抛出一个异常,解释发生了什么。这里有一个例子:

试图将值空白分配给参数方法"SomeMethod"的"p1",并且没有任何保护条款阻止这一点。你是不是错过了保护条款?

您也可以使用AutoFixture.Idioms而不需要参数化测试,只需自己实例化对象即可:

[Fact]
public void Should_throw_argument_exception_when_the_input_strings_are_invalid()
{
    var assertion = new ValidatesTheStringArguments(new Fixture());
    var sut = typeof(SystemUnderTest).GetMethod("SomeMethod");
    assertion.Verify(sut);
}