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

C#如何使用AutoFixture简化单元测试字符串参数,c#,unit-testing,autofixture,C#,Unit Testing,Autofixture,我试图创建一种在单元测试中测试字符串参数的简单方法,对于大多数字符串参数,我想检查参数为Null、空或仅包含空格时的行为 在大多数情况下,我使用string.IsNullOrWhiteSpace()检查参数,如果它有这三个值之一,则抛出异常 现在对于单元测试,我似乎必须为每个字符串参数编写三个单元测试。一个用于空值,一个用于空值,一个用于空白 想象一个有3或4个字符串参数的方法,然后我需要编写9或12个单元测试 有人能想出一个简单的方法来测试这个吗?也许可以使用AutoFixture?来避免多次

我试图创建一种在单元测试中测试字符串参数的简单方法,对于大多数字符串参数,我想检查参数为Null、空或仅包含空格时的行为

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

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

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

有人能想出一个简单的方法来测试这个吗?也许可以使用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));
}
只有
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));
}
根据
NullReferenceBehaviorExpection
EmptyStringBehaviorExpection
WhiteSpaceOnlyStringBehaviorExpection
中表达的期望,AutoFixture将自动尝试使用
null
调用名为
“SomeMethod”
的方法,分别为空字符串和空白

如果该方法没有抛出正确的异常(如expectation类中的
catch
块中指定的),则AutoFixture本身将抛出一个异常,解释发生了什么。下面是一个例子:

试图将值空白指定给参数 方法“SomeMethod”的“p1”,并且没有任何保护条款阻止这一点。 你是否遗漏了一个保护条款

您也可以使用AutoFixture.idiom,而无需通过自己实例化对象进行参数化测试:

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

搜索AutoFixture习惯用法-它很好地支持您在OPF中描述的特定保护条款。虽然这当然是一种可能性,但出于以下几个原因,我不推荐这种方法。首先,OP要求验证的不仅仅是
null
guards,AutoFixture.Idioms提供了现成的支持,因此需要编写新的行为预期。当然,这并不难。第二,AFAIK AutoFixture。习惯用法要求通过反射按名称调用方法。第三,我认为习语虽然很有用,但往往会将断言逻辑与测试分离得太远,这是以牺牲测试为代价的,尽管如此,它仍然是一个可行的解决方案。为了完整起见,我把它加到了我的答案中。感谢@RubenBartelink指出这一点。@Enricoampidoglio Re#2,习语提供了从程序集到方法名称字符串的不同级别的重载,而最低级别的重载确实在
MethodInfo
级别起作用,关键在于,它使您能够针对不同的项目集做出广泛的断言-您根本不会对s的单个方法进行
guardClauseSertion
风格测试。Re#3,我要反驳的是,能够从一组测试中提取出保护子句测试的噪音,让它们专注于强调sut的预期行为,而不是在某些语言/API样式中依赖原语可能需要的繁忙工作和样板文件,这实际上是非常有价值的。例如,可以定义一个测试方法来检查
GuardClauseAssertion
类型的所有方法/属性,并且当您添加/删除/重构时,您不需要调整busywork测试以匹配。感谢您的回答,唯一的“问题”是我们使用MSTest,在AF.Idioms示例中,您[间接]使用
AutoData
生成
sut
s。从内存来看,惯用语还提供了一个层,可以对断言进行完整的端到端验证(您可以提供一个夹具/样本生成器,甚至可以构建它)-寻址@Dennis addressed(即,您不需要使用“AutoData
)。这对于guard子句测试是有意义的,无论如何,您不应该依赖于通常体现在
AutoData`[-派生属性]中的任何其他约定。@Dennis as Ruben Bartelink正确地说,您也可以使用AutoFixture.Idioms而不使用参数化测试。我用一个例子更新了我的答案。
[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");
    }
}
[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);
}