如何实现 XUnit 的详细断言消息?

64

背景

我在XUnit的github上发现了这个问题:Add Assert.Equal(expected, actual, message) overload #350 (所以一个开发人员要求添加一个不存在的重载,如下所示)

回答中引用的内容:

我们相信自文档化代码;这包括您的断言。

(因此,XUnit团队拒绝了它)

好吧,我明白了。我也相信自文档化代码。但我仍然无法找到这个用例:

示例

// Arrange
// Create some external soap service client and its wrapper classes

// Act
// client.SomeMethod();

// Assert
// Sorry, soap service's interface, behaviour and design is *given*
// So I have to check if there is no Error, and 
// conveniently if there is, then I would like to see it in the assertion message

Assert.Equal(0, client.ErrorMessage.Length); // Means no error

// I would like to have the same result what would be the following *N*U*n*i*t* assert:
// Assert.AreEqual(0, client.ErrorMessage.Length, client.ErrorMessage); // Means no error

问题

在XUnit中,如果还没有这样的重载,我该如何实现一个描述性的断言消息?


我不清楚问题出在哪里。你为什么不像你在评论中指出的那样使用Assert.AreEqual(0, client.ErrorMessage.Length, client.ErrorMessage);呢? - Kritner
2
XUnit中没有这样的重载。那是一个NUnit调用。请看最开始的句子:开发人员要求这样的重载,但XUnit团队因引用的“我们相信自我记录的代码;包括您的断言”而拒绝了。 - g.pickardou
1
@g.pickardou,为什么不使用链接中提供的建议呢?比如流畅的断言或创建自己的断言来包装 Assert.TrueAssert.False,它们保留了其消息重载。在下面进一步提到了“您可以为 Assert.True 和 .False 提供消息。如果您简单地无法没有消息而生活(并拒绝使用其他断言),您总是可以退回到:Assert.True(number == 2, "This is my message");” - Nkosi
1
@Nikosi:因为我没有理解那个 :-),虽然这是一个回答,但我仍然没有找到/理解你所指的流畅示例。 - g.pickardou
14
有趣的是,XUnit维护者锁定了你提到的问题单,以确保他们不必再听到有关此功能的投票(在表示已经做出决定后)。这个功能的优势很明显:在测试失败时发出测试状态。代码显然可以自我说明,但仍然可以从发出输出中受益,因为输出不必像XUnit的假设那样被硬编码。Assert.True("所有输出都必须" == "硬编码")失败了。我刚开始过渡到该库,并且本来可以处理这个缺陷;但是不能接受那种态度。 - shannon
显示剩余2条评论
4个回答

45
使用链接提供的建议。例如流畅的断言,或创建自己的断言,将Assert.True或Assert.False包装起来,这些断言已经带有其消息重载。如下所述。

引用

You can provide messages to Assert.True and .False. If you simply cannot live without messages (and refuse to use a different assertion), you could always fall back to:

Assert.True(number == 2, "This is my message");

引用:

如果你真的想要消息,你可以在测试项目中添加Fluent Assertions或者xbehave,然后使用它们的语法。Fluent Assertions甚至会在遇到它的存在时抛出xunit.net异常。


这是一个答案,不过在你的评论中提到的流畅样例我仍然没有找到/获取到。 - g.pickardou
1
花了一些时间,但最终我搞定了。(这是亿万级单元测试框架,我必须立即开始使用它...) - g.pickardou
7
回退到Assert.True(number == 2, "This is my message");会导致一个更加模糊的消息,隐藏了实际比较的值:"This is my message <newline> Expected: True <newline> Actual: False" 因此这并不是一个真正的选择(当然我并不是在抱怨你@Nkosi,只是觉得需要指出来 :)) - yair
3
尽管 @TabsNotSpaces 的建议确实有所改善,但您仍将获得一个更长的消息,其中包括两组期望/实际值对:“这是我的消息<newline>期望值:2 <newline> 实际值:1 <newline> 期望值:True <newline> 实际值:False”。感谢您的建议 :) 尊重 Xunit,无论他们想表达多少意见,我认为“自我记录代码”与单位测试输出在我们的 CI/CD 管道日志上无关... :) - yair
1
这只是一种解决方法,而不是一个完美的解决方案或替代品。使用 fluent-validations(其本身就很糟糕)会使你失去错误信息中所有好用的“期望值/实际值”提示。 - t3chb0t
显示剩余2条评论

18

我遇到了同样的问题。我的测试需要从两个Web API中获取数据,然后比较并断言内容的各个方面。我开始使用标准的XUnit断言,例如:

Assert.Equal(HttpStatusCode.OK, response1.StatusCode);
Assert.Equal(HttpStatusCode.OK, response2.StatusCode);

虽然这个提示信息说明了404错误已经返回,但是从我们的构建/CI服务器日志中并不清楚哪个服务引起了这个错误信息。

最终,我添加了自己的断言来提供上下文:

public class MyEqualException : Xunit.Sdk.EqualException
{
    public MyEqualException(object expected, object actual, string userMessage)
        : base(expected, actual)
    {
        UserMessage = userMessage;
    }

    public override string Message => UserMessage + "\n" + base.Message;
}

public static class AssertX
{
    /// <summary>
    /// Verifies that two objects are equal, using a default comparer.
    /// </summary>
    /// <typeparam name="T">The type of the objects to be compared</typeparam>
    /// <param name="expected">The expected value</param>
    /// <param name="actual">The value to be compared against</param>
    /// <param name="userMessage">Message to show in the error</param>
    /// <exception cref="MyEqualException">Thrown when the objects are not equal</exception>
    public static void Equal<T>(T expected, T actual, string userMessage)
    {
        bool areEqual;

        if (expected == null || actual == null)
        {
            // If either null, equal only if both null
            areEqual = (expected == null && actual == null);
        }
        else
        {
            // expected is not null - so safe to call .Equals()
            areEqual = expected.Equals(actual);
        }

        if (!areEqual)
        {
            throw new MyEqualException(expected, actual, userMessage);
        }
    }
}

那么我可以进行相同的断言,如下:

AssertX.Equal(HttpStatusCode.OK, response1.StatusCode, $"Fetching {Uri1}");
AssertX.Equal(HttpStatusCode.OK, response2.StatusCode, $"Fetching {Uri2}");

错误日志会给出实际值、期望值并在我的信息前缀中指出是哪个WebAPI导致的问题。

我意识到我回答有点晚,但我认为这可能会帮助其他搜索实用解决方案的人,他们没有时间安装/学习另一个测试框架,只是想从测试失败中获得有用的信息。


自定义的Equal<T>方法抛出EqualException而不是您扩展的MyEqualException。 - Dave Thompson
7
我开始怀疑我选择 xUnit 而不是 MSTest 是否明智。后者提供了更好的断言选项。你有什么想法吗? - toughQuestions

9

对于我的目的,使用try/catch就足够了:

try
{
    Assert.Equal(expectedErrorCount, result.Count);
}
catch (EqualException ex)
{
    throw new XunitException($"{testMsg}\n{ex}");
}

我相信这是最好的答案;尽管我更喜欢并使用 FluentAssertions。 - LosManos

5

我遇到了同样的问题,甚至在六年之后,没有人遵循建议编写自定义断言方法,这让我感到惊讶。所以我自己写了一个这里

只需添加nuget包并像这样别名AssertM类:

using Assert = XunitAssertMessages.AssertM;

所有之前的xunit断言方法都可用,所以当前的断言将继续编译,但具有一个添加的可选消息参数。

// This will work
Assert.Equal(0, client.ErrorMessage.Length)
// and so will this
Assert.Equal(0, client.ErrorMessage.Length, "Unexpected length")

1
有几件事需要注意。(1) CS8602在单元测试中应考虑使用Assert.NotNull,(2) AssertM.Equal("expected", "actual") 是含糊不清的。 太棒了!这个包真是太有帮助了。 - Doug Domeny
@DougDomeny 感谢您的宝贵反馈。这两个问题都已在最新的2.4.1版本中得到修复。 - GaussZ
1
“不明确”的错误仍在发生。但我确认第一个问题已经解决了。感谢快速响应。 例如,AssertM.Equal(paramType.Name, actualType.Name);(其中Name是字符串) 错误:CS0121 调用在以下方法或属性之间不明确: 'AssertM.Equal<T>(T, T, string?)' 和 'AssertM.Equal(string?, string?, bool, bool, bool, string?)' 这不是阻塞问题,因为我有一个失败消息,但它并不严格兼容。 - Doug Domeny
1
@DougDomeny 哇,再次感谢!我完全错过了那个。现在应该在2.4.2中修复了。 - GaussZ

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