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

183

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()
{
    // ...
}

您可以使用MSTestHacks,如https://dev59.com/5XRC5IYBdhLWcg3wSu97#19536942答案中所述。 - Michael Freidgeim
可能是如何在MSTest中使用RowTest?的重复问题。 - Michael Freidgeim
@MichaelFreidgeim,这个问题的答案比你建议的目标更好。 - Rob
1
@Rob: 我认为,这个问题缺少最合适的回答—MSTestHacks-如何使用MSTest进行RowTest? - Michael Freidgeim
@MichaelFreidgeim 或许吧,不过看起来这个功能已经存在了3年半了(https://dev59.com/0mox5IYBdhLWcg3wtmc1#13710788) - Rob
@Rob:仅在WinRT/Metro的单元测试项目中可用。请参见https://visualstudio.uservoice.com/forums/330519-team-services/suggestions/3865310-allow-use-of-datatestmethod-datarow-in-all-unit。 - Michael Freidgeim
11个回答

208

编辑4: 看起来这个功能已经在2016年6月17日的MSTest V2中完成了: https://blogs.msdn.microsoft.com/visualstudioalm/2016/06/17/taking-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 Team(产品团队,Microsoft Visual Studio)已回复 · 2016年4月25日感谢您的反馈。我们已经开始着手解决此问题。

Pratap Lakshman Visual Studio

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


4
现在,使用Visual Studio 2012 Update 2(目前是CTP 4版本),Windows Phone也得到了支持。 - Pedro Lamas
9
我已更新第一个版本,但DataTestMethod和DataRow未被识别,这些属性在哪个库中? - DevDave
3
有没有关于DataTestMethod的官方资料?它在哪个命名空间和程序集中? - Igor Lankin
3
我发现在我的电脑上安装了UnitTestFramework.dll,手动引用它后,我可以使用[DataTestMethod]属性和数据行编写一个方法,但是我无法让Visual Studio 2012.3中的测试资源管理器找到这个方法。 - Josh DeLong
5
我在电脑上访问了文件路径"C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0\ExtensionSDKs\MSTestFramework\11.0\References\CommonConfiguration\neutral\Microsoft.VisualStudio.TestPlatform.UnitTestFramework.dll",并发现该文件存在。因此我在我的基本单元测试项目中引用了它。使用JustDecompile打开该dll文件后发现,该库只引用了mscorlib、System和System.Core。这不是一个Windows Store项目。 - Josh DeLong
显示剩余6条评论

72
这个功能现在处于预发布状态,且与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);
    }
}

4
这是正确的答案。请注意,使用 [DataRow] 不需要说 [DataTestMethod] (https://dev59.com/rlYN5IYBdhLWcg3wx6sR#59162403)。 - Design.Garden
2
这是正确的答案,应该被接受!对我真的很有用!谢谢! - Anduin Xue
[DataRow(1, 2, 3)] ?? - JohnB
@JohnB 可能是分别对应参数 abresult - timelmer
1
我认为第一组测试数据是错误的。我认为应该是:[DataRow(1, 2, 3)] - JohnB
在我将dll添加到您的引用点后,该dll与我的项目引用“Microsoft.VisualStudio.QulityTools.UnitTestFrameWrok”发生冲突,因此我将其删除。虽然“DataRow”被接受了,但是我的测试资源管理器无法找到这些测试。 - Jun Yu

49

我们正在使用Selenium生成NUnit代码,因此我们转而使用NUnit :) - The Light
4
FYI,我发现Visual Studio 2012 Update 1现在可以做类似的事情,供将来考虑该答案的任何人参考。 - McAden
@McAden 你有一个带有解释的链接吗? - jeroenh
7
我在下面给出了一个例子和链接到我的博客文章的答案。它提到了必要的属性以及属性上的“DisplayName”属性,该属性区分了测试资源管理器中的不同情况。这也在 CTP 的十月公告中提到(现在已正式发布)http://blogs.msdn.com/b/visualstudioalm/archive/2012/10/26/final-ctp-for-visual-studio-2012-update-1.aspx 。我将这些信息添加到此 SO 问题中,因为我花了很多时间寻找它。希望这能为某人节省一些时间。 - McAden

13

虽然与NUnit的Value(或TestCase)属性不完全相同,但MSTest具有DataSource属性,可以让您执行类似的操作。

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


7

5

我在 Visual Studio 2015 中无法使用 DataRowAttribute,最终采用了以下方法:

[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中)。


4
你应该将每个测试调用拆分为单独的测试,以节省时间,当其中一个测试失败时,这将会发生(我们都知道这会发生)。 - silver
当然可以。实际上,这就是实现的方式。在这种情况下,我只是为了简单起见进行了说明。 - Brandon

5

这很容易实现 - 你应该使用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);
        });
}

请查看扩展方法和一组示例以获取更多详细信息:methodssamples

2
这种方法可行,但不会为每组参数创建单独的测试用例。 - usr4896260
您可以使用更复杂的TestProperty值(例如“0-100”),并在测试主体中解析和处理它。 - Andrey Burykin

4

4

当然,还有另一种方法可以实现这个功能,这在本主题中没有讨论过,即通过继承包含TestMethod的类来实现。在下面的例子中,只定义了一个TestMethod,但是制作了两个测试用例。

在Visual Studio 2012中,它会在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) { }
    }
    

1
OP的例子是关于NUnit的一个特性,它可以轻松地生成提供的值的笛卡尔积。据我所知,这里没有人回答到这一部分。我把它看作是一个小挑战,并最终得到了以下的实现。
[编辑:基于数组的重构+Zip值]
我对原始的基于Enumerator的版本进行了一些重构(请参阅帖子历史),现在只使用数组和循环遍历索引。我还利用这个机会添加了一种新的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
}

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