我们如何在 MSTest 中运行具有多个参数的测试方法?

发布于 2024-12-29 02:09:34 字数 424 浏览 0 评论 0原文

NUnit 有一个名为 Values 的功能,如下所示:

[Test]
public void MyTest(
    [Values(1,2,3)] int x,
    [Values("A","B")] string s)
{
    // ...
}

这意味着测试方法将运行六次:

MyTest(1, "A")
MyTest(1, "B")
MyTest(2, "A")
MyTest(2, "B")
MyTest(3, "A")
MyTest(3, "B")

我们现在使用 MSTest,但是是否有任何等效项,以便我可以使用多个参数运行相同的测试?

[TestMethod]
public void Mytest()
{
    // ...
}

NUnit has a feature called Values, like below:

[Test]
public void MyTest(
    [Values(1,2,3)] int x,
    [Values("A","B")] string s)
{
    // ...
}

This means that the test method will run six times:

MyTest(1, "A")
MyTest(1, "B")
MyTest(2, "A")
MyTest(2, "B")
MyTest(3, "A")
MyTest(3, "B")

We're using MSTest now, but is there any equivalent for this so that I can run the same test with multiple parameters?

[TestMethod]
public void Mytest()
{
    // ...
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

回心转意 2025-01-05 02:09:34

编辑 4:看起来这是在 2016 年 6 月 17 日的 MSTest V2 中完成的:https://blogs.msdn.microsoft .com/visualstudioalm/2016/06/17/take-the-mstest-framework-forward-with-mstest-v2/

原始答案

截至大约一周前,在 Visual Studio 2012 Update 1 中,现在可以实现类似的操作:

[DataTestMethod]
[DataRow(12,3,4)]
[DataRow(12,2,6)]
[DataRow(12,4,3)]
public void DivideTest(int n, int d, int q)
{
  Assert.AreEqual( q, n / d );
}

编辑:看来这仅在 WinRT/Metro 的单元测试项目中可用。糟糕的

编辑 2:以下是在 Visual Studio 中使用“转到定义”找到的元数据:

#region Assembly Microsoft.VisualStudio.TestPlatform.UnitTestFramework.dll, v11.0.0.0
// C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0\ExtensionSDKs\MSTestFramework\11.0\References\CommonConfiguration\neutral\Microsoft.VisualStudio.TestPlatform.UnitTestFramework.dll
#endregion

using System;

namespace Microsoft.VisualStudio.TestPlatform.UnitTestFramework
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class DataTestMethodAttribute : TestMethodAttribute
    {
        public DataTestMethodAttribute();

        public override TestResult[] Execute(ITestMethod testMethod);
    }
}

编辑 3:此问题是在 Visual Studio 的 UserVoice 论坛中提出的。
最后更新指出:

已开始 · Visual Studio 团队管理员 Visual Studio 团队(产品
团队(Microsoft Visual Studio)已回复 · 2016 年 4 月 25 日 谢谢
以获得反馈。我们已经开始致力于此。

普拉塔普·拉克什曼 Visual Studio

https://visualstudio.uservoice.com/forums/330519-team-services/suggestions/3865310-allow-use-of-datatestmethod-datarow-in-all-unit

EDIT 4: Looks like this is completed in MSTest V2 June 17, 2016: https://blogs.msdn.microsoft.com/visualstudioalm/2016/06/17/taking-the-mstest-framework-forward-with-mstest-v2/

Original Answer:

As of about a week ago in Visual Studio 2012 Update 1 something similar is now possible:

[DataTestMethod]
[DataRow(12,3,4)]
[DataRow(12,2,6)]
[DataRow(12,4,3)]
public void DivideTest(int n, int d, int q)
{
  Assert.AreEqual( q, n / d );
}

EDIT: It appears this is only available within the unit testing project for WinRT/Metro. Bummer

EDIT 2: The following is the metadata found using "Go To Definition" within Visual Studio:

#region Assembly Microsoft.VisualStudio.TestPlatform.UnitTestFramework.dll, v11.0.0.0
// C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0\ExtensionSDKs\MSTestFramework\11.0\References\CommonConfiguration\neutral\Microsoft.VisualStudio.TestPlatform.UnitTestFramework.dll
#endregion

using System;

namespace Microsoft.VisualStudio.TestPlatform.UnitTestFramework
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class DataTestMethodAttribute : TestMethodAttribute
    {
        public DataTestMethodAttribute();

        public override TestResult[] Execute(ITestMethod testMethod);
    }
}

EDIT 3: This issue was brought up in Visual Studio's UserVoice forums.
Last Update states:

STARTED · Visual Studio Team ADMIN Visual Studio Team (Product
Team, Microsoft Visual Studio) responded · April 25, 2016 Thank you
for the feedback. We have started working on this.

Pratap Lakshman Visual Studio

https://visualstudio.uservoice.com/forums/330519-team-services/suggestions/3865310-allow-use-of-datatestmethod-datarow-in-all-unit

一城柳絮吹成雪 2025-01-05 02:09:34

此功能位于 现已预发布,可与 Visual Studio 2015 配合使用。

例如:

[TestClass]
public class UnitTest1
{
    [TestMethod]
    [DataRow(1, 2, 2)]
    [DataRow(2, 3, 5)]
    [DataRow(3, 5, 8)]
    public void AdditionTest(int a, int b, int result)
    {
        Assert.AreEqual(result, a + b);
    }
}

This feature is in pre-release now and works with Visual Studio 2015.

For example:

[TestClass]
public class UnitTest1
{
    [TestMethod]
    [DataRow(1, 2, 2)]
    [DataRow(2, 3, 5)]
    [DataRow(3, 5, 8)]
    public void AdditionTest(int a, int b, int result)
    {
        Assert.AreEqual(result, a + b);
    }
}
总以为 2025-01-05 02:09:34

不幸的是,旧版本的 MSTest 不支持它。显然有一个 可扩展性模型,您可以自己实现。另一种选择是使用 数据驱动的测试

我个人的意见是坚持使用 NUnit...

从 Visual Studio 2012 更新 1 开始,MSTest 具有类似的功能。请参阅McAden 的回答

It is unfortunately not supported in older versions of MSTest. Apparently there is an extensibility model and you can implement it yourself. Another option would be to use data-driven tests.

My personal opinion would be to just stick with NUnit though...

As of Visual Studio 2012, update 1, MSTest has a similar feature. See McAden's answer.

信愁 2025-01-05 02:09:34

与 NUnit 的 Value (或 TestCase)属性不完全相同,但 MSTest 具有 DataSource 属性,它允许您执行类似的操作。

您可以将其连接到数据库或 XML 文件 - 它不像 NUnit 的功能那么简单,但它可以完成工作。

Not exactly the same as NUnit's Value (or TestCase) attributes, but MSTest has the DataSource attribute, which allows you to do a similar thing.

You can hook it up to database or XML file - it is not as straightforward as NUnit's feature, but it does the job.

柒夜笙歌凉 2025-01-05 02:09:34

MSTest 有一个强大的属性,称为 DataSource。使用它,您可以按照您的要求执行数据驱动的测试。您可以将测试数据保存在 XML、CSV 或数据库中。以下是一些可以指导您的链接

MSTest has a powerful attribute called DataSource. Using this you can perform data-driven tests as you asked. You can have your test data in XML, CSV, or in a database. Here are few links that will guide you

许一世地老天荒 2025-01-05 02:09:34

实现起来非常简单 - 您应该使用 TestContext 属性和 TestPropertyAttribute

示例

public TestContext TestContext { get; set; }
private List<string> GetProperties()
{
    return TestContext.Properties
        .Cast<KeyValuePair<string, object>>()
        .Where(_ => _.Key.StartsWith("par"))
        .Select(_ => _.Value as string)
        .ToList();
}

//usage
[TestMethod]
[TestProperty("par1", "http://getbootstrap.com/components/")]
[TestProperty("par2", "http://www.wsj.com/europe")]
public void SomeTest()
{
    var pars = GetProperties();
    //...
}

编辑:

我准备了一些扩展方法来简化对 TestContext 属性的访问,并且就像我们有几个测试用例一样。请参阅此处处理简单测试属性的示例:

[TestMethod]
[TestProperty("fileName1", @".\test_file1")]
[TestProperty("fileName2", @".\test_file2")]
[TestProperty("fileName3", @".\test_file3")]
public void TestMethod3()
{
    TestContext.GetMany<string>("fileName").ForEach(fileName =>
    {
        //Arrange
        var f = new FileInfo(fileName);

        //Act
        var isExists = f.Exists;

        //Asssert
        Assert.IsFalse(isExists);
    });
}

以及创建复杂测试对象的示例:

[TestMethod]
//Case 1
[TestProperty(nameof(FileDescriptor.FileVersionId), "673C9C2D-A29E-4ACC-90D4-67C52FBA84E4")]
//...
public void TestMethod2()
{
    //Arrange
    TestContext.For<FileDescriptor>().Fill(fi => fi.FileVersionId).Fill(fi => fi.Extension).Fill(fi => fi.Name).Fill(fi => fi.CreatedOn, new CultureInfo("en-US", false)).Fill(fi => fi.AccessPolicy)
        .ForEach(fileInfo =>
        {
            //Act
            var fileInfoString = fileInfo.ToString();

            //Assert
            Assert.AreEqual($"Id: {fileInfo.FileVersionId}; Ext: {fileInfo.Extension}; Name: {fileInfo.Name}; Created: {fileInfo.CreatedOn}; AccessPolicy: {fileInfo.AccessPolicy};", fileInfoString);
        });
}

查看扩展 方法示例了解更多详细信息。

It's very simple to implement - you should use TestContext property and TestPropertyAttribute.

Example

public TestContext TestContext { get; set; }
private List<string> GetProperties()
{
    return TestContext.Properties
        .Cast<KeyValuePair<string, object>>()
        .Where(_ => _.Key.StartsWith("par"))
        .Select(_ => _.Value as string)
        .ToList();
}

//usage
[TestMethod]
[TestProperty("par1", "http://getbootstrap.com/components/")]
[TestProperty("par2", "http://www.wsj.com/europe")]
public void SomeTest()
{
    var pars = GetProperties();
    //...
}

EDIT:

I prepared few extension methods to simplify access to the TestContext property and act like we have several test cases. See example with processing simple test properties here:

[TestMethod]
[TestProperty("fileName1", @".\test_file1")]
[TestProperty("fileName2", @".\test_file2")]
[TestProperty("fileName3", @".\test_file3")]
public void TestMethod3()
{
    TestContext.GetMany<string>("fileName").ForEach(fileName =>
    {
        //Arrange
        var f = new FileInfo(fileName);

        //Act
        var isExists = f.Exists;

        //Asssert
        Assert.IsFalse(isExists);
    });
}

and example with creating complex test objects:

[TestMethod]
//Case 1
[TestProperty(nameof(FileDescriptor.FileVersionId), "673C9C2D-A29E-4ACC-90D4-67C52FBA84E4")]
//...
public void TestMethod2()
{
    //Arrange
    TestContext.For<FileDescriptor>().Fill(fi => fi.FileVersionId).Fill(fi => fi.Extension).Fill(fi => fi.Name).Fill(fi => fi.CreatedOn, new CultureInfo("en-US", false)).Fill(fi => fi.AccessPolicy)
        .ForEach(fileInfo =>
        {
            //Act
            var fileInfoString = fileInfo.ToString();

            //Assert
            Assert.AreEqual(
quot;Id: {fileInfo.FileVersionId}; Ext: {fileInfo.Extension}; Name: {fileInfo.Name}; Created: {fileInfo.CreatedOn}; AccessPolicy: {fileInfo.AccessPolicy};", fileInfoString);
        });
}

Take a look to the extension methods and set of samples for more details.

瞄了个咪的 2025-01-05 02:09:34

我无法让 DataRowAttribute 在 Visual Studio 2015 中工作,这就是我最终得到的结果:

[TestClass]
public class Tests
{
    private Foo _toTest;

    [TestInitialize]
    public void Setup()
    {
        this._toTest = new Foo();
    }

    [TestMethod]
    public void ATest()
    {
        this.Perform_ATest(1, 1, 2);
        this.Setup();

        this.Perform_ATest(100, 200, 300);
        this.Setup();

        this.Perform_ATest(817001, 212, 817213);
        this.Setup();
    }

    private void Perform_ATest(int a, int b, int expected)
    {
        // Obviously this would be way more complex...

        Assert.IsTrue(this._toTest.Add(a,b) == expected);
    }
}

public class Foo
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

这里真正的解决方案是仅使用 NUnit(除非您像我一样陷入 MSTest 中)在这个特定的例子中)。

I couldn't get The DataRowAttribute to work in Visual Studio 2015, and this is what I ended up with:

[TestClass]
public class Tests
{
    private Foo _toTest;

    [TestInitialize]
    public void Setup()
    {
        this._toTest = new Foo();
    }

    [TestMethod]
    public void ATest()
    {
        this.Perform_ATest(1, 1, 2);
        this.Setup();

        this.Perform_ATest(100, 200, 300);
        this.Setup();

        this.Perform_ATest(817001, 212, 817213);
        this.Setup();
    }

    private void Perform_ATest(int a, int b, int expected)
    {
        // Obviously this would be way more complex...

        Assert.IsTrue(this._toTest.Add(a,b) == expected);
    }
}

public class Foo
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

The real solution here is to just use NUnit (unless you're stuck in MSTest like I am in this particular instance).

月亮坠入山谷 2025-01-05 02:09:34

当然,还有另一种方法可以做到这一点,但本线程中尚未讨论,即通过继承包含 TestMethod 的类。在下面的示例中,仅定义了一个 TestMethod,但创建了两个测试用例。

在 Visual Studio 2012 中,它在 TestExplorer 中创建两个测试:

  1. DemoTest_B10_A5.test
  2. DemoTest_A12_B4.test

    公共类演示
    {
        整数a,b;
    
        公共演示(int _a,int _b)
        {
            这个.a = _a;
            这个.b = _b;
        }
    
        公共 int Sum()
        {
            返回 this.a + this.b;
        }
    }
    
    公共抽象类 DemoTestBase
    {
        演示 objUnderTest;
        int 预期总和;
    
        公共 DemoTestBase(int _a, int _b, int _expectedSum)
        {
            objUnderTest = new Demo(_a, _b);
            this.expectedSum = _expectedSum;
        }
    
        [测试方法]
        公共无效测试()
        {
            Assert.AreEqual(this.expectedSum, this.objUnderTest.Sum());
        }
    }
    
    [测试类]
    公共类 DemoTest_A12_B4 :DemoTestBase
    {
        公共 DemoTest_A12_B4() : 基数(12, 4, 16) { }
    }
    
    公共抽象类 DemoTest_B10_Base : DemoTestBase
    {
        公共 DemoTest_B10_Base(int _a) : 基(_a, 10, _a + 10) { }
    }
    
    [测试类]
    公共类 DemoTest_B10_A5 :DemoTest_B10_Base
    {
        公共 DemoTest_B10_A5() : 基(5) { }
    }
    

There is, of course, another way to do this which has not been discussed in this thread, i.e. by way of inheritance of the class containing the TestMethod. In the following example, only one TestMethod has been defined but two test cases have been made.

In Visual Studio 2012, it creates two tests in the TestExplorer:

  1. DemoTest_B10_A5.test
  2. DemoTest_A12_B4.test

    public class Demo
    {
        int a, b;
    
        public Demo(int _a, int _b)
        {
            this.a = _a;
            this.b = _b;
        }
    
        public int Sum()
        {
            return this.a + this.b;
        }
    }
    
    public abstract class DemoTestBase
    {
        Demo objUnderTest;
        int expectedSum;
    
        public DemoTestBase(int _a, int _b, int _expectedSum)
        {
            objUnderTest = new Demo(_a, _b);
            this.expectedSum = _expectedSum;
        }
    
        [TestMethod]
        public void test()
        {
            Assert.AreEqual(this.expectedSum, this.objUnderTest.Sum());
        }
    }
    
    [TestClass]
    public class DemoTest_A12_B4 : DemoTestBase
    {
        public DemoTest_A12_B4() : base(12, 4, 16) { }
    }
    
    public abstract class DemoTest_B10_Base : DemoTestBase
    {
        public DemoTest_B10_Base(int _a) : base(_a, 10, _a + 10) { }
    }
    
    [TestClass]
    public class DemoTest_B10_A5 : DemoTest_B10_Base
    {
        public DemoTest_B10_A5() : base(5) { }
    }
    
在巴黎塔顶看东京樱花 2025-01-05 02:09:34

MSTest 不支持该功能,但您可以实现自己的属性来实现该功能。

查看在 MSTest 中启用参数化测试使用 PostSharp

MSTest does not support that feature, but you can implement your own attribute to achieve that.

Have a look at Enabling parameterized tests in MSTest using PostSharp.

攀登最高峰 2025-01-05 02:09:34

OP 示例是关于 NUnit 功能的,该功能可以轻松地获得所提供值的笛卡尔积。据我所知,这里没有答案涵盖该部分。我认为这是一个小小的挑战,并最终实现了以下实施。

[编辑:基于数组的重构 + Zip 值]

我对原始的基于枚举器的版本进行了一些重构(请参阅帖子历史记录),现在仅使用数组和循环索引。我还借此机会添加了新的 Zip 类型的值,该值将与笛卡尔生成创建的每个集合匹配不同的值。例如,这对于添加 ExpectedResult 可能很有用。

它仍然没有真正优化,所以请随意提出改进建议。

#nullable enable
public enum ValuesType
{
    Undefined = 0,
    Cartesian = 1,
    /// <summary>
    /// Values will be <see cref="Enumerable.Zip{TFirst, TSecond, TResult}(IEnumerable{TFirst}, IEnumerable{TSecond}, Func{TFirst, TSecond, TResult})">Zipped</see> with the cartesian produce of the other parameters.
    /// </summary>
    Zip = 2
}

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class ValuesAttribute : Attribute
{
    public ValuesType ValuesType { get; }
    public object[] Values { get; }

    public ValuesAttribute(params object[] values)
        : this(ValuesType.Cartesian, values)
    { }

    public ValuesAttribute(ValuesType valuesType, params object[] values)
    {
        ValuesType = valuesType;
        Values = values;
    }
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ValuesDataSourceAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object[]> GetData(MethodInfo methodInfo)
    {
        var parameters = methodInfo.GetParameters();
        var values = new (ValuesType Type, object[] Values, int Index)[parameters.Length];
        for(var i=0; i < parameters.Length; i++)
        {
            var parameter = parameters[i];
            var attribute = parameter.GetCustomAttribute<ValuesAttribute>();
            if (attribute != null)
            {
                if (attribute.Values.Any(v => !parameter.ParameterType.IsAssignableFrom(v.GetType())))
                    throw new InvalidOperationException($"All values of {nameof(ValuesAttribute)} must be of type {parameter.ParameterType.Name}. ParameterName: {parameter.Name}.");

                switch (attribute.ValuesType)
                {
                    case ValuesType.Cartesian:
                        values[i] = (ValuesType.Cartesian, attribute.Values, 0);
                        break;
                    case ValuesType.Zip:
                        values[i] = (ValuesType.Zip, attribute.Values, 0);
                        break;
                }
            }
            else if (parameter.ParameterType == typeof(bool))
                values[i] = (ValuesType.Cartesian, new object[] { false, true }, 0);
            else if (parameter.ParameterType.IsEnum)
                values[i] = (ValuesType.Cartesian, Enum.GetValues(parameter.ParameterType).Cast<Object>().ToArray(), 0);
            else
                throw new InvalidOperationException($"All parameters must have either {nameof(ValuesAttribute)} attached or be a bool or an Enum . ParameterName: {parameter.Name}.");
        }

        //Since we are using ValueTuples, it is essential that once we created our collection, we stick to it. If we were to create a new one, we would end up with a copy of the ValueTuples that won't be synced anymore.
        var cartesianTotalCount = values.Where(v => v.Type == ValuesType.Cartesian).Aggregate(1, (actualTotal, currentValues) => actualTotal * currentValues.Values.Length);
        if (values.Any(v => v.Type == ValuesType.Zip && v.Values.Length != cartesianTotalCount))
            throw new InvalidOperationException($"{nameof(ValuesType.Zip)} typed attributes must have as many values as the produce of all the others ({cartesianTotalCount}).");

        bool doIncrement;
        for(var globalPosition = 0; globalPosition < cartesianTotalCount; globalPosition++)
        {
            yield return values.Select(v => v.Values[v.Index]).ToArray();
            doIncrement = true;
            for (var i = values.Length - 1; i >= 0 && doIncrement; i--)
            {
                switch (values[i].Type)
                {
                    case ValuesType.Zip:
                        values[i].Index++;
                        break;
                    case ValuesType.Cartesian:
                        if (doIncrement && ++values[i].Index >= values[i].Values.Length)
                            values[i].Index = 0;
                        else
                            doIncrement = false;
                        break;
                    default:
                        throw new InvalidOperationException($"{values[i].Type} is not supported.");
                }
            }
        }
    }

    public string GetDisplayName(MethodInfo methodInfo, object[] data)
    {
        return data.JoinStrings(" / ");
    }
}

用法:

[TestMethod]
[ValuesDataSource]
public void Test([Values("a1", "a2")] string a, [Values(1, 2)] int b, bool c, System.ConsoleModifiers d, [Values(ValuesType.Zip, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)] int asserts)
{
    //Arrange / Act / Assert
    //Cases would be
    // a1, 1, false, System.ConsoleModifiers.Alt, 1
    // a1, 1, false, System.ConsoleModifiers.Shift, 2
    // a1, 1, false, System.ConsoleModifiers.Control, 3
    // a1, 1, true, System.ConsoleModifiers.Alt, 4
    // a1, 1, true, System.ConsoleModifiers.Shift, 5
    // a1, 1, true, System.ConsoleModifiers.Control, 6
    // a1, 2, false, System.ConsoleModifiers.Alt, 7
    // a1, 2, false, System.ConsoleModifiers.Shift, 8
    // a1, 2, false, System.ConsoleModifiers.Control, 9
    // a1, 2, true, System.ConsoleModifiers.Alt, 10
    // a1, 2, true, System.ConsoleModifiers.Shift, 11
    // a1, 2, true, System.ConsoleModifiers.Control, 12
    // a2, 1, false, System.ConsoleModifiers.Alt, 13
    // a2, 1, false, System.ConsoleModifiers.Shift, 14
    // a2, 1, false, System.ConsoleModifiers.Control, 15
    // a2, 1, true, System.ConsoleModifiers.Alt, 16
    // a2, 1, true, System.ConsoleModifiers.Shift, 17
    // a2, 1, true, System.ConsoleModifiers.Control, 18
    // a2, 2, false, System.ConsoleModifiers.Alt, 19
    // a2, 2, false, System.ConsoleModifiers.Shift, 20
    // a2, 2, false, System.ConsoleModifiers.Control, 21
    // a2, 2, true, System.ConsoleModifiers.Alt, 22
    // a2, 2, true, System.ConsoleModifiers.Shift, 23
    // a2, 2, true, System.ConsoleModifiers.Control, 24
}

The OP example was about an NUnit feature that easily allows to have a cartesian product of provided values. As far as I was able to tell, no answer here covered that part. I saw this as a little challenge and ended up with the following implementation.

[Edit: Array based refactoring + Zip values]

I did some refactoring to the original Enumerator based version (see post history) to now use only Arrays and loop throuch indices instead. I also took the opporunity to add a new Zip type of values that will match a different value to every set created by the cartesian produce. This may be useful to add an ExpectedResult for instance.

It is still not really optimized so feel free to suggest improvements.

#nullable enable
public enum ValuesType
{
    Undefined = 0,
    Cartesian = 1,
    /// <summary>
    /// Values will be <see cref="Enumerable.Zip{TFirst, TSecond, TResult}(IEnumerable{TFirst}, IEnumerable{TSecond}, Func{TFirst, TSecond, TResult})">Zipped</see> with the cartesian produce of the other parameters.
    /// </summary>
    Zip = 2
}

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class ValuesAttribute : Attribute
{
    public ValuesType ValuesType { get; }
    public object[] Values { get; }

    public ValuesAttribute(params object[] values)
        : this(ValuesType.Cartesian, values)
    { }

    public ValuesAttribute(ValuesType valuesType, params object[] values)
    {
        ValuesType = valuesType;
        Values = values;
    }
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ValuesDataSourceAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object[]> GetData(MethodInfo methodInfo)
    {
        var parameters = methodInfo.GetParameters();
        var values = new (ValuesType Type, object[] Values, int Index)[parameters.Length];
        for(var i=0; i < parameters.Length; i++)
        {
            var parameter = parameters[i];
            var attribute = parameter.GetCustomAttribute<ValuesAttribute>();
            if (attribute != null)
            {
                if (attribute.Values.Any(v => !parameter.ParameterType.IsAssignableFrom(v.GetType())))
                    throw new InvalidOperationException(
quot;All values of {nameof(ValuesAttribute)} must be of type {parameter.ParameterType.Name}. ParameterName: {parameter.Name}.");

                switch (attribute.ValuesType)
                {
                    case ValuesType.Cartesian:
                        values[i] = (ValuesType.Cartesian, attribute.Values, 0);
                        break;
                    case ValuesType.Zip:
                        values[i] = (ValuesType.Zip, attribute.Values, 0);
                        break;
                }
            }
            else if (parameter.ParameterType == typeof(bool))
                values[i] = (ValuesType.Cartesian, new object[] { false, true }, 0);
            else if (parameter.ParameterType.IsEnum)
                values[i] = (ValuesType.Cartesian, Enum.GetValues(parameter.ParameterType).Cast<Object>().ToArray(), 0);
            else
                throw new InvalidOperationException(
quot;All parameters must have either {nameof(ValuesAttribute)} attached or be a bool or an Enum . ParameterName: {parameter.Name}.");
        }

        //Since we are using ValueTuples, it is essential that once we created our collection, we stick to it. If we were to create a new one, we would end up with a copy of the ValueTuples that won't be synced anymore.
        var cartesianTotalCount = values.Where(v => v.Type == ValuesType.Cartesian).Aggregate(1, (actualTotal, currentValues) => actualTotal * currentValues.Values.Length);
        if (values.Any(v => v.Type == ValuesType.Zip && v.Values.Length != cartesianTotalCount))
            throw new InvalidOperationException(
quot;{nameof(ValuesType.Zip)} typed attributes must have as many values as the produce of all the others ({cartesianTotalCount}).");

        bool doIncrement;
        for(var globalPosition = 0; globalPosition < cartesianTotalCount; globalPosition++)
        {
            yield return values.Select(v => v.Values[v.Index]).ToArray();
            doIncrement = true;
            for (var i = values.Length - 1; i >= 0 && doIncrement; i--)
            {
                switch (values[i].Type)
                {
                    case ValuesType.Zip:
                        values[i].Index++;
                        break;
                    case ValuesType.Cartesian:
                        if (doIncrement && ++values[i].Index >= values[i].Values.Length)
                            values[i].Index = 0;
                        else
                            doIncrement = false;
                        break;
                    default:
                        throw new InvalidOperationException(
quot;{values[i].Type} is not supported.");
                }
            }
        }
    }

    public string GetDisplayName(MethodInfo methodInfo, object[] data)
    {
        return data.JoinStrings(" / ");
    }
}

Usage:

[TestMethod]
[ValuesDataSource]
public void Test([Values("a1", "a2")] string a, [Values(1, 2)] int b, bool c, System.ConsoleModifiers d, [Values(ValuesType.Zip, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)] int asserts)
{
    //Arrange / Act / Assert
    //Cases would be
    // a1, 1, false, System.ConsoleModifiers.Alt, 1
    // a1, 1, false, System.ConsoleModifiers.Shift, 2
    // a1, 1, false, System.ConsoleModifiers.Control, 3
    // a1, 1, true, System.ConsoleModifiers.Alt, 4
    // a1, 1, true, System.ConsoleModifiers.Shift, 5
    // a1, 1, true, System.ConsoleModifiers.Control, 6
    // a1, 2, false, System.ConsoleModifiers.Alt, 7
    // a1, 2, false, System.ConsoleModifiers.Shift, 8
    // a1, 2, false, System.ConsoleModifiers.Control, 9
    // a1, 2, true, System.ConsoleModifiers.Alt, 10
    // a1, 2, true, System.ConsoleModifiers.Shift, 11
    // a1, 2, true, System.ConsoleModifiers.Control, 12
    // a2, 1, false, System.ConsoleModifiers.Alt, 13
    // a2, 1, false, System.ConsoleModifiers.Shift, 14
    // a2, 1, false, System.ConsoleModifiers.Control, 15
    // a2, 1, true, System.ConsoleModifiers.Alt, 16
    // a2, 1, true, System.ConsoleModifiers.Shift, 17
    // a2, 1, true, System.ConsoleModifiers.Control, 18
    // a2, 2, false, System.ConsoleModifiers.Alt, 19
    // a2, 2, false, System.ConsoleModifiers.Shift, 20
    // a2, 2, false, System.ConsoleModifiers.Control, 21
    // a2, 2, true, System.ConsoleModifiers.Alt, 22
    // a2, 2, true, System.ConsoleModifiers.Shift, 23
    // a2, 2, true, System.ConsoleModifiers.Control, 24
}
满地尘埃落定 2025-01-05 02:09:34

这是 NUnits [Combinatorial][Sequential][Values] 的重新实现。

与默认情况下采用 [Combinatorial] 的 NUnit 不同,在 MSTest 中,我们必须始终指定我们想要的,否则 [Values ] 属性不会有任何效果。

用法:

[TestClass]
public class TestMethods
{
    [TestMethod, Combinatorial]
    public void EnumIterationTestMethod(Season season) => Console.WriteLine(season);

    [TestMethod, Combinatorial]
    public void BoolIterationTestMethod(bool boolean) => Console.WriteLine(boolean);

    [TestMethod, Combinatorial]
    public void CombinatoralValuesIterationTestMethod(Season season, bool boolean) => Console.WriteLine($"{season} {boolean}");

    [TestMethod, Sequential]
    public void SequentialCombinatoralIterationTestMethod(
    [Values(1, 2, 3)] int param1,
    [Values("A", "B")] string param2) => Console.WriteLine($"{param1} {param2 ?? "null"}");

    [TestMethod, Combinatorial]
    public void CombinatoralIterationTestMethod(
    [Values(1, 2, 3)] int param1,
    [Values("A", "B")] string param2) => Console.WriteLine($"{param1} {param2 ?? "null"}");
}

代码:

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class ValuesAttribute : Attribute
{
    public object?[] Values { get; }

    public ValuesAttribute(params object?[] values)
    {
        Values = values;
    }
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CombinatorialAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
    {
        var values = Utils.GetPossibleValuesForEachParameter(methodInfo);
        return Utils.CreateCombinations(values);
    }

    public string? GetDisplayName(MethodInfo methodInfo, object?[]? data)
    {
        if (data != null)
        {
            return $"{methodInfo.Name} ({string.Join(", ", data.Select(e => e ?? "null"))})";
        }

        return null;
    }
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class SequentialAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
    {
        var values = Utils.GetPossibleValuesForEachParameter(methodInfo);
        return Utils.ZipLongestFillWithNull(values);
    }

    public string? GetDisplayName(MethodInfo methodInfo, object?[]? data)
    {
        if (data != null)
        {
            return $"{methodInfo.Name} ({string.Join(", ", data.Select(e => e ?? "null"))})";
        }

        return null;
    }
}

public static class Utils
{
    public static List<List<object?>> GetPossibleValuesForEachParameter(MethodInfo methodInfo)
    {
        List<List<object?>> values = new();

        foreach (var parameter in methodInfo.GetParameters())
        {
            var attribute = parameter.GetCustomAttribute<ValuesAttribute>();

            if (attribute == null || attribute.Values.Length == 0)
            {
                if (parameter.ParameterType.IsEnum)
                {
                    values.Add(Enum.GetValues(parameter.ParameterType).Cast<object?>().ToList());
                    continue;
                }

                if (parameter.ParameterType == typeof(bool))
                {
                    values.Add(new List<object?> { true, false });
                    continue;
                }

                if (attribute == null)
                {
                    throw new InvalidOperationException($"{parameter.Name} should have a [Values(...)] attribute set");
                }
                else
                {
                    throw new InvalidOperationException($"[Values] {parameter.ParameterType} {parameter.Name} is only valid for Enum or Boolean types. Consider using the attribute constructor [Values(...)].");
                }
            }

            values.Add(attribute.Values.ToList());
        }

        return values;
    }

    public static IEnumerable<object?[]> ZipLongestFillWithNull(List<List<object?>> values)
    {
        var longest = values.Max(e => e.Count);

        foreach (var list in values)
        {
            if (list.Count < longest)
            {
                var diff = longest - list.Count;
                list.AddRange(Enumerable.Repeat<object?>(null, diff));
            }
        }

        for (int i = 0; i < longest; i++)
        {
            yield return values.Select(e => e[i]).ToArray();
        }
    }

    public static IEnumerable<object?[]> CreateCombinations(List<List<object?>> values)
    {
        var indices = new int[values.Count];

        while (true)
        {
            // Create new arguments
            var arg = new object?[indices.Length];
            for (int i = 0; i < indices.Length; i++)
            {
                arg[i] = values[i][indices[i]];
            }

            yield return arg!;

            // Increment indices
            for (int i = indices.Length - 1; i >= 0; i--)
            {
                indices[i]++;
                if (indices[i] >= values[i].Count)
                {
                    indices[i] = 0;

                    if (i == 0)
                        yield break;
                }
                else
                    break;
            }
        }
    }
}

Here is a reimplentation of NUnits [Combinatorial], [Sequential] and [Values].

Unlike NUnit, which assumes [Combinatorial] by default, in MSTest we must always specify which one we want, else the [Values] attribute will not have any effect.

Usage:

[TestClass]
public class TestMethods
{
    [TestMethod, Combinatorial]
    public void EnumIterationTestMethod(Season season) => Console.WriteLine(season);

    [TestMethod, Combinatorial]
    public void BoolIterationTestMethod(bool boolean) => Console.WriteLine(boolean);

    [TestMethod, Combinatorial]
    public void CombinatoralValuesIterationTestMethod(Season season, bool boolean) => Console.WriteLine(
quot;{season} {boolean}");

    [TestMethod, Sequential]
    public void SequentialCombinatoralIterationTestMethod(
    [Values(1, 2, 3)] int param1,
    [Values("A", "B")] string param2) => Console.WriteLine(
quot;{param1} {param2 ?? "null"}");

    [TestMethod, Combinatorial]
    public void CombinatoralIterationTestMethod(
    [Values(1, 2, 3)] int param1,
    [Values("A", "B")] string param2) => Console.WriteLine(
quot;{param1} {param2 ?? "null"}");
}

Code:

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class ValuesAttribute : Attribute
{
    public object?[] Values { get; }

    public ValuesAttribute(params object?[] values)
    {
        Values = values;
    }
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CombinatorialAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
    {
        var values = Utils.GetPossibleValuesForEachParameter(methodInfo);
        return Utils.CreateCombinations(values);
    }

    public string? GetDisplayName(MethodInfo methodInfo, object?[]? data)
    {
        if (data != null)
        {
            return 
quot;{methodInfo.Name} ({string.Join(", ", data.Select(e => e ?? "null"))})";
        }

        return null;
    }
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class SequentialAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
    {
        var values = Utils.GetPossibleValuesForEachParameter(methodInfo);
        return Utils.ZipLongestFillWithNull(values);
    }

    public string? GetDisplayName(MethodInfo methodInfo, object?[]? data)
    {
        if (data != null)
        {
            return 
quot;{methodInfo.Name} ({string.Join(", ", data.Select(e => e ?? "null"))})";
        }

        return null;
    }
}

public static class Utils
{
    public static List<List<object?>> GetPossibleValuesForEachParameter(MethodInfo methodInfo)
    {
        List<List<object?>> values = new();

        foreach (var parameter in methodInfo.GetParameters())
        {
            var attribute = parameter.GetCustomAttribute<ValuesAttribute>();

            if (attribute == null || attribute.Values.Length == 0)
            {
                if (parameter.ParameterType.IsEnum)
                {
                    values.Add(Enum.GetValues(parameter.ParameterType).Cast<object?>().ToList());
                    continue;
                }

                if (parameter.ParameterType == typeof(bool))
                {
                    values.Add(new List<object?> { true, false });
                    continue;
                }

                if (attribute == null)
                {
                    throw new InvalidOperationException(
quot;{parameter.Name} should have a [Values(...)] attribute set");
                }
                else
                {
                    throw new InvalidOperationException(
quot;[Values] {parameter.ParameterType} {parameter.Name} is only valid for Enum or Boolean types. Consider using the attribute constructor [Values(...)].");
                }
            }

            values.Add(attribute.Values.ToList());
        }

        return values;
    }

    public static IEnumerable<object?[]> ZipLongestFillWithNull(List<List<object?>> values)
    {
        var longest = values.Max(e => e.Count);

        foreach (var list in values)
        {
            if (list.Count < longest)
            {
                var diff = longest - list.Count;
                list.AddRange(Enumerable.Repeat<object?>(null, diff));
            }
        }

        for (int i = 0; i < longest; i++)
        {
            yield return values.Select(e => e[i]).ToArray();
        }
    }

    public static IEnumerable<object?[]> CreateCombinations(List<List<object?>> values)
    {
        var indices = new int[values.Count];

        while (true)
        {
            // Create new arguments
            var arg = new object?[indices.Length];
            for (int i = 0; i < indices.Length; i++)
            {
                arg[i] = values[i][indices[i]];
            }

            yield return arg!;

            // Increment indices
            for (int i = indices.Length - 1; i >= 0; i--)
            {
                indices[i]++;
                if (indices[i] >= values[i].Count)
                {
                    indices[i] = 0;

                    if (i == 0)
                        yield break;
                }
                else
                    break;
            }
        }
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文