为什么我的理论单元测试在测试资源管理器摘要中显示 "test has multiple result outcomes"?

Why does my Theory unit test say "test has multiple result outcomes" in the test explorer summary?

通常,当我用 [Theory] 修饰 XUnit 测试方法并使用 [InlineData][ClassData] 等属性将多组参数传递到测试中时, Visual Studio 测试资源管理器 window 让我扩展测试方法,将每组参数视为独立的测试。

但是,我有一个行为不同的测试 - 它在项目、命名空间、classes、测试方法等的树视图中显示为单个测试,但是在测试详细信息摘要窗格中它说“测试有多个结果结果”,可以是通过和失败的混合。这使得更难准确地看到什么通过或失败,并且还阻止我调试仅使用一组参数的测试。

这是我的单元测试:

using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using Xunit;

public class MyTestClass
{
    [Theory]
    [ClassData(typeof(MyTestData1))]
    public void MyTestMethod1(Color col, int red, int green, int blue)
    {
        Assert.Equal(red, col.R);
        Assert.Equal(green, col.G);
        Assert.Equal(blue, col.B);
    }
}

提供我测试数据的class:

public class MyTestData1 : IEnumerable<object[]>
{
    public MyTestData1()
    {
        this.Data = new List<object[]>();
        this.Data.Add(new object[] { Color.FromArgb(255, 0, 0), 255, 0, 0 });
        this.Data.Add(new object[] { Color.FromArgb(0, 255, 0), 0, 255, 0 });
        this.Data.Add(new object[] { Color.FromArgb(0, 0, 255), 0, 0, 255 });
    }

    public List<object[]> Data { get; }
    public IEnumerator<object[]> GetEnumerator() => this.Data.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}

是的,这是 System.Drawing.Color 的单元测试,我不需要为此编写自己的单元测试,因为它由 Microsoft 维护,因此他们负责对其进行单元测试。然而,它作为一个最小可重现的例子很好地工作,因为我在我的测试参数中使用那个特定结构的事实似乎是我在测试资源管理器中的行为与我期望的测试之间的唯一区别't。在我遇到这个问题的实际项目中,我使用 Color 的数组作为参数,但问题似乎是一样的。

我需要做什么才能让我在测试资源管理器中扩展这个测试,这样我就可以 run/debug 它只用一组参数?

我已经解决了,但找到正确的搜索词并不容易,所以我提出并回答了这个问题,希望下一个遇到这个问题的人能更容易找到它奇怪的行为。

突破是在我发现这个GitHub问题时Question: Why do some theories get expanded into individual test cases and others not? #1473

In the Visual Studio test runner, test cases are discovered in one process, and executed in another. Therefore, test cases must be able to be turned into an unqualified string representation (aka, "serialized") in order to be run. We can also serialize at the test method level, because that just involves knowing the type and method name (both strings). When you start putting data into the mix, we need to ensure we know how to serialize that data; if we can't serialize all of the data for a theory, then we have to fall back to just a single method (which we know we can serialize).

所以是的,问题是我使用 System.Drawing.Color 作为测试参数,因为 System.Drawing.Color 不可序列化,所以 Xunit 无法提供关于我的测试用例的足够信息测试资源管理器的测试资源管理器以我期望的方式显示它们。所以我需要使用 System.Drawing.Color.

以外的其他类型

因为我唯一使用 Color 结构的是一个容器来容纳颜色的红色、绿色和蓝色分量,所以我可以使用 3 intbyte 参数来表示这些组件,但我不想走那条路,特别是当我使用颜色数组时,我想将一种颜色的所有 3 个组件放在一个对象中,并且我我希望我的代码尽可能具有可读性,尤其是它们之间的那 3 个数字代表一种颜色。

在这种情况下对我有用的解决方案是创建我自己的 class,它实现了 System.Drawing.Color 所做的足以满足我的测试需求的功能,但它也实现了 IXunitSerializable 以便 Xunit 知道如何序列化它。

public class Colour : IXunitSerializable
{
    public byte R { get; set; }
    public byte G { get; set; }
    public byte B { get; set; }

    public static Colour FromArgb(byte r, byte g, byte b)
    {
        return new Colour { R = r, G = g, B = b };
    }

    public static Colour FromArgb(int r, int g, int b)
    {
        return new Colour
        {
            R = (byte)r,
            G = (byte)g,
            B = (byte)b,
        };
    }

    public void Deserialize(IXunitSerializationInfo info)
    {
        this.R = info.GetValue<byte>(nameof(this.R));
        this.G = info.GetValue<byte>(nameof(this.G));
        this.B = info.GetValue<byte>(nameof(this.B));
    }

    public void Serialize(IXunitSerializationInfo info)
    {
        info.AddValue(nameof(this.R), this.R);
        info.AddValue(nameof(this.G), this.G);
        info.AddValue(nameof(this.B), this.B);
    }
}

当我更改我的单元测试和测试数据 class 以使用此 Colour class 而不是 System.Drawing.Color 时,一切都如我所料,包括能够在测试资源管理器中展开测试,这样我就可以 运行 它只使用测试数据 class.

提供的一组参数

Colour class 的重要部分是 Serialize 方法(创建对象的纯文本表示)和 Deserialize 方法(创建实例对象的纯文本表示)。

Serialize 方法中,我们要存储当前 Colour 实例的属性,这些属性是重新创建 Colour 的相同实例所必需的。我们通过将键(属性 的名称)和值(属性 的值)传递给 IXunitSerializationInfo 实例的 AddValue 方法来实现。

Deserialize 方法中,我们 运行 在 Colour class 的实例的上下文中,其无参数构造函数已被调用,但没有其他已设置。因此,我们需要将当前实例的属性设置为我们可以从提供的 IXunitSerializationInfo 对象中获取的值,方法是将每个 属性 的键(在本例中为 属性 名称)传递给GetValue 方法。

您可以使用任何字符串作为调用 GetValueAddValue 的键,只要您在保存和获取相同的 属性 时使用相同的字符串即可。出于这个原因,我认为使用 属性 的名称作为键更简单。