前言
虽然,推荐做法是每次测试只断言一件事,但是,在实际工作中,我们可能需要对同一个对象同时执行多个断言。
例如,微软官方示例项目eShopOnContainers
有一个测试用例的实现代码如下:
[Fact]
public async Task Add_to_cart_success()
{
//Arrange
var fakeCatalogItem = GetFakeCatalogItem();
_basketServiceMock.Setup(x => x.AddItemToBasket(It.IsAny<ApplicationUser>(), It.IsAny<Int32>()))
.Returns(Task.FromResult(1));
//Act
var orderController = new CartController(_basketServiceMock.Object, _catalogServiceMock.Object, _identityParserMock.Object);
orderController.ControllerContext.HttpContext = _contextMock.Object;
var actionResult = await orderController.AddToCart(fakeCatalogItem);
//Assert
Assert.Equal("Catalog", redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
}
如果我们将所有断言都改成必定失败:
//Assert
Assert.Equal("WrongCatalog", redirectToActionResult.ControllerName);
Assert.Equal("WrongIndex", redirectToActionResult.ActionName);
但运行测试时,只会提示第一个失败的断言:
在这种情况下,一个个断言去验证并修正比较耗时。
如果同时能够看到所有失败的断言则更加有帮助。
手工实现
查看`Assert.Equal`的实现代码[1],它是通过抛出EqualException
异常来表明断言失败:
public static void Equal<T>(T expected, T actual, IEqualityComparer<T> comparer)
{
GuardArgumentNotNull(nameof(comparer), comparer);
var expectedAsIEnum = expected as IEnumerable;
var actualAsIEnum = actual as IEnumerable;
// If both are IEnumerable (or null), see if we got an AssertEqualityComparer<T>, so that
// we can invoke it to get the mismatched index.
if ((expectedAsIEnum != null && (actual == null || actualAsIEnum != null)) ||
(actualAsIEnum != null && expected == null))
{
var aec = comparer as AssertEqualityComparer<T>;
int? mismatchedIndex;
if (aec != null && !aec.Equals(expected, actual, out mismatchedIndex))
{
if (mismatchedIndex.HasValue)
throw EqualException.FromEnumerable(expectedAsIEnum, actualAsIEnum, mismatchedIndex.Value);
else
throw new EqualException(expected, actual);
}
}
if (!comparer.Equals(expected, actual))
throw new EqualException(expected, actual);
}
而所有断言异常都继承自基类XunitException
:
因此,我们可以捕获每个断言的异常,然后将多个异常添加到集合中,在测试结束时再抛出:
var xunitExceptions = new List<XunitException>();
try
{
Assert.Equal("WrongCatalog", redirectToActionResult.ControllerName);
}
catch (XunitException ex)
{
xunitExceptions.Add(ex);
}
try
{
Assert.Equal("WrongIndex", redirectToActionResult.ActionName);
}
catch (XunitException ex)
{
xunitExceptions.Add(ex);
}
if (xunitExceptions.Any())
{
throw new AggregateException(xunitExceptions.ToArray());
}
这里虽然列出了所有失败断言,但是所有错误显示了2遍,而且我们必须为测试编写大量的try-catch代码。
有不有更好的方法呢?!
FluentAssertions
FluentAssertions是一组.NET扩展方法,允许用更自然的语法去验证断言。
引用Nuget包FluentAssertions
,示例的断言可以修改成如下格式:
redirectToActionResult.ControllerName.Should().Be("WrongCatalog");
redirectToActionResult.ActionName.Should().Be("WrongIndex");
除此之外,还可以将多个断言放到一个AssertionScope中,以便FluentAssertions在所有失败的范围末尾抛出一个异常:
using (new FluentAssertions.Execution.AssertionScope())
{
redirectToActionResult.ControllerName.Should().Be("WrongCatalog");
redirectToActionResult.ActionName.Should().Be("WrongIndex");
}
结论
有时,将多个断言组合在一起测试是有意义的。
在这种情况下,可以使用FluentAssertions的AssertionScope
来编写此类测试。
如果你觉得这篇文章对你有所启发,请关注我的个人公众号”My IO“
参考资料
[1]
Assert.Equal
的实现代码: https://github.com/xunit/assert.xunit/blob/main/EqualityAsserts.cs#L78