使用XUnit断言异常

259

我是XUnit和Moq的新手。我有一个接受字符串参数的方法。如何使用XUnit处理异常。

[Fact]
public void ProfileRepository_GetSettingsForUserIDWithInvalidArguments_ThrowsArgumentException() {
    //arrange
    ProfileRepository profiles = new ProfileRepository();
    //act
    var result = profiles.GetSettingsForUserID("");
    //assert
    //The below statement is not working as expected.
    Assert.Throws<ArgumentException>(() => profiles.GetSettingsForUserID(""));
}

被测试的方法

public IEnumerable<Setting> GetSettingsForUserID(string userid)
{            
    if (string.IsNullOrWhiteSpace(userid)) throw new ArgumentException("User Id Cannot be null");
    var s = profiles.Where(e => e.UserID == userid).SelectMany(e => e.Settings);
    return s;
}

1
“is not working as expected” 是什么意思?(另外,请更加易读地格式化您的代码。使用预览功能,在您阅读时看起来符合您期望的样子后再发布。) - Jon Skeet
5
提示:在开始调用Assert.Throws之前,您正在调用GetSettingsForUserID("")Assert.Throws 调用不能帮助您。我建议您在 AAA 方面更加灵活... - Jon Skeet
5个回答

433

Assert.Throws表达式可以捕获异常并断言其类型。然而,由于你在断言表达式之外调用了测试方法,导致测试用例失败。

[Fact]
public void ProfileRepository_GetSettingsForUserIDWithInvalidArguments_ThrowsArgumentException()
{
    //arrange
    ProfileRepository profiles = new ProfileRepository();
    // act & assert
    Assert.Throws<ArgumentException>(() => profiles.GetSettingsForUserID(""));
}

如果你想一心追随AAA,可以将该行为提取到自己的变量中。

[Fact]
public void ProfileRepository_GetSettingsForUserIDWithInvalidArguments_ThrowsArgumentException()
{
    //arrange
    ProfileRepository profiles = new ProfileRepository();
    //act
    Action act = () => profiles.GetSettingsForUserID("");
    //assert
    ArgumentException exception = Assert.Throws<ArgumentException>(act);
    //The thrown exception can be used for even more detailed assertions.
    Assert.Equal("expected error message here", exception.Message);
}

请注意异常也可用于更详细的断言。

如果进行异步测试,Assert.ThrowsAsync与之前给出的示例类似,只是应该等待断言完成。

public async Task Some_Async_Test() {

    //...

    //Act
    Func<Task> act = () => subject.SomeMethodAsync();

    //Assert
    var exception = await Assert.ThrowsAsync<InvalidOperationException>(act);

    //...
}

76

如果您想在AAA模式下更加严格,那么您可以使用xUnit中的Record.Exception来捕获Act阶段中的异常。

然后,您可以基于捕获的异常在Assert阶段进行断言。

这个例子可以在xUnit测试中看到。

[Fact]
public void Exception()
{
    Action testCode = () => { throw new InvalidOperationException(); };

    var ex = Record.Exception(testCode);

    Assert.NotNull(ex);
    Assert.IsType<InvalidOperationException>(ex);
}

你可以选择自己想要走的道路,而 xUnit 提供了对这两种道路的充分支持。


1
说实话,如果你需要验证异常信息等内容,这个解决方案非常棒。我认为这就是你可能会使用Record.Exception的时候。 - Jeff LaFay
1
@JeffLaFay,我有点晚来参加派对了,那种方法与使用var exception = Assert.Throws<InvalidOperationException>(testCode);并断言exception.Message有什么不同?还是说这只是实现相同目的的另一种方式? - ColinM
1
请参考此答案,了解如何在异步情况下使用Record.ExceptionAsync - LosManos
1
如果您需要断言抛出异常,但又不想或无法具体指定异常类型,这是一个很好的解决方案。 - DhyMik
@ColinM 我来晚了,但这种方法允许清楚地区分Act和Assert,并能够断言特定于异常类型的属性(而不是在“Exception”类型上可用的属性)。 - vc 74

9
如果你想坚持使用AAA,可以考虑像这样做:
// Act 
Task act() => handler.Handle(request);

// Assert
await Assert.ThrowsAsync<MyExpectedException>(act);

4

我认为有两种处理这种情况的方式,个人比较喜欢的是以下方法。假设我有下面的方法需要测试

    public class SampleCode
    {
       public void GetSettingsForUserID(string userid)
       {
          if (string.IsNullOrWhiteSpace(userid)) throw new ArgumentException("User Id 
             Cannot be null");
          // Some code 
       }
    }

我可以使用以下测试用例进行测试,请确保在测试项目中添加FluentAssertions NuGet包。
    public class SampleTest
    {
        private SampleCode _sut;

        public SampleTest()
        {
           _sut = new SampleCode();
        }

        [Theory]
        [InlineData(null)]
        [InlineData("    ")]
        public void TestIfValueIsNullorwhiteSpace(string userId)
        {
            //Act
            Action act= ()=> _sut.GetSettingsForUserID(userId);
             
            // Assert
            act.Should().ThrowExactly<ArgumentException>().WithMessage("User Id Cannot be null");

        }
    }

但是我在这里发现一个问题,空格和Null是两个不同的事情。C#为空格提供了ArgumentException,而对于空引用则提供ArgumentNullException。
因此,您可以重构您的代码,就像这样:
    public void GetSettingsForUserID(string userid)
    {
        Guard.Against.NullOrWhiteSpace(userid, nameof(userid));
    }

您的代码项目中需要使用Ardalis.GuardClauses nuget。测试用例应该像这样:

    [Fact]
    public void TestIfValueIsNull()
    {
        //Act
        Action act = () => _sut.GetSettingsForUserID(null);
        
        //Assert
        act.Should().ThrowExactly<ArgumentNullException>().WithMessage("*userId*");

    }

    [Fact]
    public void TestIfValueIsWhiteSpace()
    {
        //Act
        Action act= ()=> _sut.GetSettingsForUserID("        ");
        
        //Assert
        act.Should().ThrowExactly<ArgumentException>().WithMessage("*userId*");
    }

2

与其遵循复杂的协议,我发现使用try catch块最为方便:

try
{
    var output = Settings.GetResultFromIActionResult<int>(controller.CreateAllFromExternalAPI());
    Assert.True(output > 0);
}
catch(InvalidOperationException e)
{
    Assert.True("Country table can only be filled from ExternalAPI if table is blank"==e.Message);
}

如果你正在阅读这篇文章,请不要编写像这样的测试。遵循协议,它们存在是有原因的。 Try catch 块应该用于处理异常,而不是测试。 即使你不想使用库,你也应该使用 var ex = Assert.Throws<TException>(action); - Vinicius Bassi
1
它存在是有原因的,你能否请强调一下这个原因?每一件事都有其存在的原因,而编写相同代码的替代方法也是存在的。 它在这里运行良好且没有任何影响。如果我错了,请告诉我原因。 - Sujoy
1
有一些框架专门用于模拟测试用例和断言特定结果,例如抛出异常并断言它是否被抛出。try catch块不是用于测试目的,而是用于处理执行时的异常。 如果您想要抛出异常并断言它们是否被抛出,您应该使用专门设计的框架,例如Moq和FluentAssertions。 - Vinicius Bassi

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