C# 在VisualStudio中控制单元测试的执行顺序
好了,我已经找不到这方面的好消息了。 我有一系列的单元测试,它们调用一个静态类,该类一旦初始化,就会设置不能(或者我不希望)更改的属性 我的问题是我无法强制执行测试运行的设置顺序。如果可以的话,我可以以这样的方式运行它们,因为静态属性将以可靠的方式设置,并且我可以对它们进行断言,但不幸的是,Microsoft.VisualStudio.TestTools.UnitTesting框架只是以看似随机的顺序运行它们 所以,我在备注部分找到了这样一条:“这个属性不是由测试系统使用的,它是为用户定制的。”哈?那有什么用呢?他们是否希望我编写自己的测试包装器来利用这个惊人的属性(如果我想达到这个级别,我可以很容易地编写自己的属性…) 所以,别再唠叨了;总之,有没有办法控制单元测试运行的顺序C# 在VisualStudio中控制单元测试的执行顺序,c#,unit-testing,visual-studio-2012,C#,Unit Testing,Visual Studio 2012,好了,我已经找不到这方面的好消息了。 我有一系列的单元测试,它们调用一个静态类,该类一旦初始化,就会设置不能(或者我不希望)更改的属性 我的问题是我无法强制执行测试运行的设置顺序。如果可以的话,我可以以这样的方式运行它们,因为静态属性将以可靠的方式设置,并且我可以对它们进行断言,但不幸的是,Microsoft.VisualStudio.TestTools.UnitTesting框架只是以看似随机的顺序运行它们 所以,我在备注部分找到了这样一条:“这个属性不是由测试系统使用的,它是为用户定制的。”
[TestMethod]
[Priority(0)]
等似乎不起作用,这是有道理的,因为微软说它不会
此外,请不要对“违反隔离”发表评论。TestClass隔离了我正在测试的内容,而不是单个的TestMethods。不管怎样,每个测试都可以独立运行,但它们不能以随机顺序一起运行,因为没有办法破坏静态类
哦,我还知道“有序测试”。将您的测试合并到一个大型测试中会有效。为了使测试方法更具可读性,可以执行以下操作
[TestMethod]
public void MyIntegratonTestLikeUnitTest()
{
AssertScenarioA();
AssertScenarioB();
....
}
private void AssertScenarioA()
{
// Assert
}
private void AssertScenarioB()
{
// Assert
}
实际上,您遇到的问题建议您可能应该改进实现的可测试性 既然您已经提到了VisualStudio测试框架提供的有序测试功能,我将忽略这一点。您似乎还意识到,为了测试这个静态类而试图完成的工作是一个“坏主意”,所以我将忽略这一点 相反,让我们把重点放在如何确保您的测试按照您想要的顺序执行。一个选项(由@gaog提供)是“一个测试方法,多个测试函数”,在标记有
TestMethod
属性的单个函数中,按照您想要的顺序调用测试函数。这是最简单的方法,唯一的缺点是第一个失败的测试函数将阻止任何剩余的测试函数执行
根据您对情况的描述,这是我建议您使用的解决方案
如果粗体部分对您来说是个问题,那么您可以通过利用内置的数据驱动测试功能来完成隔离测试的有序执行。它更复杂,感觉有点脏,但它完成了任务
简而言之,您可以定义一个数据源(如CSV文件或数据库表),该数据源控制运行测试的顺序以及实际包含测试功能的函数的名称。然后将该数据源挂接到数据驱动测试中,使用顺序读取选项,并以所需的顺序执行函数,作为单独的测试
[TestClass]
public class OrderedTests
{
public TestContext TestContext { get; set; }
private const string _OrderedTestFilename = "TestList.csv";
[TestMethod]
[DeploymentItem(_OrderedTestFilename)]
[DataSource("Microsoft.VisualStudio.TestTools.DataSource.CSV", _OrderedTestFilename, _OrderedTestFilename, DataAccessMethod.Sequential)]
public void OrderedTests()
{
var methodName = (string)TestContext.DataRow[0];
var method = GetType().GetMethod(methodName);
method.Invoke(this, new object[] { });
}
public void Method_01()
{
Assert.IsTrue(true);
}
public void Method_02()
{
Assert.IsTrue(false);
}
public void Method_03()
{
Assert.IsTrue(true);
}
}
在我的示例中,我有一个名为TestList.csv的支持文件,它被复制到输出。看起来是这样的:
TestName
Method_01
Method_02
Method_03
[TestMethod]
public void AName1()
{}
[TestMethod]
public void BName2()
{}
您的测试将按照您指定的顺序执行,并且处于正常的测试隔离状态(即,如果一个测试失败,其余的测试仍将执行,但共享静态类)
以上只是基本的想法,如果我在生产中使用它,我会在测试运行之前动态地生成测试函数名及其顺序。也许可以利用您找到的PriorityAttribute和一些简单的反射代码来提取类中的测试方法并对它们进行适当的排序,然后将顺序写入数据源。对不起,我不讨论测试的顺序。其他人已经这样做了。此外,如果您了解“有序测试”,那么这就是MS VS对问题的响应。我知道那些有序的测试毫无乐趣。但他们认为这将是“它”,而MSTest中并没有更多关于这一点的内容 我写下了你的一个假设: 因为没有办法拆掉静态类 除非您的静态类表示代码外部的某个进程范围的外部状态(例如,由其余代码调用的非托管本机DLL库的状态),否则您认为
没有办法
的假设是不正确的
如果您的静态类引用了这个,那么很抱歉,您是完全正确的,这个答案的其余部分是不相关的。不过,由于您没有这样说,我假设您的代码是“托管的”
思考并检查AppDomain
内容。很少需要它,但当您可能想使用它们时,情况正是如此
您可以创建一个新的AppDomain,在那里实例化测试,并在那里运行测试方法。托管代码使用的静态数据将在那里隔离,完成后,您将能够卸载AppDomain,所有数据(包括静态数据)都将蒸发。然后,下一个测试将初始化另一个appdomain,依此类推
除非您有必须跟踪的外部状态,否则这将起作用。AppDomains仅隔离托管内存。任何本机DLL仍将按进程加载,其状态将由所有AppDomain共享
另外,创建/删除appdomains会减慢测试速度。此外,您可能在子appdomain中存在程序集解析问题,但这些问题可以通过合理数量的可重用代码来解决
此外,在将测试数据传递给子AppDomain和从子AppDomain返回时,您可能会遇到一些小问题。传递的对象要么必须以某种方式序列化,要么是MarshalByRef
等。跨域交谈几乎就像IPC
不过,请注意,这将是100%有管理的谈话。如果您采取一些额外的措施,并为应用程序添加一些工作
[IsolatedAppDomain]
void testmethod()
{
// your test code
}
[TestMethod] // place only on the list--not the individuals
public void OrderedStepsTest()
{
OrderedTest.Run(TestContext, new List<OrderedTest>
{
new OrderedTest ( T10_Reset_Database, false ),
new OrderedTest ( T20_LoginUser1, false ),
new OrderedTest ( T30_DoLoginUser1Task1, true ), // continue on failure
new OrderedTest ( T40_DoLoginUser1Task2, true ), // continue on failure
// ...
});
}
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace UnitTests.Utility
{
/// <summary>
/// Define and Run a list of ordered tests.
/// 2016/08/25: Posted to SO by crokusek
/// </summary>
public class OrderedTest
{
/// <summary>Test Method to run</summary>
public Action TestMethod { get; private set; }
/// <summary>Flag indicating whether testing should continue with the next test if the current one fails</summary>
public bool ContinueOnFailure { get; private set; }
/// <summary>Any Exception thrown by the test</summary>
public Exception ExceptionResult;
/// <summary>
/// Constructor
/// </summary>
/// <param name="testMethod"></param>
/// <param name="continueOnFailure">True to continue with the next test if this test fails</param>
public OrderedTest(Action testMethod, bool continueOnFailure = false)
{
TestMethod = testMethod;
ContinueOnFailure = continueOnFailure;
}
/// <summary>
/// Run the test saving any exception within ExceptionResult
/// Throw to the caller only if ContinueOnFailure == false
/// </summary>
/// <param name="testContextOpt"></param>
public void Run()
{
try
{
TestMethod();
}
catch (Exception ex)
{
ExceptionResult = ex;
throw;
}
}
/// <summary>
/// Run a list of OrderedTest's
/// </summary>
static public void Run(TestContext testContext, List<OrderedTest> tests)
{
Stopwatch overallStopWatch = new Stopwatch();
overallStopWatch.Start();
List<Exception> exceptions = new List<Exception>();
int testsAttempted = 0;
for (int i = 0; i < tests.Count; i++)
{
OrderedTest test = tests[i];
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
testContext.WriteLine("Starting ordered test step ({0} of {1}) '{2}' at {3}...\n",
i + 1,
tests.Count,
test.TestMethod.Method,
DateTime.Now.ToString("G"));
try
{
testsAttempted++;
test.Run();
}
catch
{
if (!test.ContinueOnFailure)
break;
}
finally
{
Exception testEx = test.ExceptionResult;
if (testEx != null) // capture any "continue on fail" exception
exceptions.Add(testEx);
testContext.WriteLine("\n{0} ordered test step {1} of {2} '{3}' in {4} at {5}{6}\n",
testEx != null ? "Error: Failed" : "Successfully completed",
i + 1,
tests.Count,
test.TestMethod.Method,
stopWatch.ElapsedMilliseconds > 1000
? (stopWatch.ElapsedMilliseconds * .001) + "s"
: stopWatch.ElapsedMilliseconds + "ms",
DateTime.Now.ToString("G"),
testEx != null
? "\nException: " + testEx.Message +
"\nStackTrace: " + testEx.StackTrace +
"\nContinueOnFailure: " + test.ContinueOnFailure
: "");
}
}
testContext.WriteLine("Completed running {0} of {1} ordered tests with a total of {2} error(s) at {3} in {4}",
testsAttempted,
tests.Count,
exceptions.Count,
DateTime.Now.ToString("G"),
overallStopWatch.ElapsedMilliseconds > 1000
? (overallStopWatch.ElapsedMilliseconds * .001) + "s"
: overallStopWatch.ElapsedMilliseconds + "ms");
if (exceptions.Any())
{
// Test Explorer prints better msgs with this hierarchy rather than using 1 AggregateException().
throw new Exception(String.Join("; ", exceptions.Select(e => e.Message), new AggregateException(exceptions)));
}
}
}
}
[TestMethod]
public void AName1()
{}
[TestMethod]
public void BName2()
{}
using NUnit.Framework;
namespace NUnit.Project
{
public class ByOrder
{
public static bool Test1Called;
public static bool Test2ACalled;
public static bool Test2BCalled;
public static bool Test3Called;
[Test, Order(5)]
public void Test1()
{
Test3Called = true;
Assert.IsTrue(Test1Called);
Assert.IsFalse(Test2ACalled);
Assert.IsTrue(Test2BCalled);
}
[Test, Order(0)]
public void Test2B()
{
Test2BCalled = true;
Assert.IsTrue(Test1Called);
Assert.IsFalse(Test2ACalled);
Assert.IsFalse(Test3Called);
}
[Test]
public void Test2A()
{
Test2ACalled = true;
Assert.IsTrue(Test1Called);
Assert.IsTrue(Test2BCalled);
Assert.IsTrue(Test3Called);
}
[Test, Order(-5)]
public void Test3()
{
Test1Called = true;
Assert.IsFalse(Test2ACalled);
Assert.IsFalse(Test2BCalled);
Assert.IsFalse(Test3Called);
}
}
}