如何在解决方案中运行所有测试

19

看起来我可以使用MSTest从命令行运行解决方案中的所有测试,只需使用/testmetadata标志,如此处所述:http://msdn.microsoft.com/en-us/library/ms182487.aspx

我正在Visual Studio 2013中运行SQL Server DB单元测试,其中似乎根本没有vsmdi文件,也找不到添加vsmdi文件的方法。我尝试创建一个testsettings文件,但在调用MSTest时它无法发现任何测试(显示“没有要运行的测试”)。

是否有办法让MSTest在VS2013解决方案中运行所有测试?

5个回答

14

我想关闭这个未解决的问题。我的意图是从我们的Hudson CI服务器一次性运行所有测试,所以我编写了一个基本的控制台应用程序来查找并调用解决方案文件夹中所有DLL文件上的MSTest。此应用程序在以发布模式构建项目后执行。

string execId = null;
string className = null;
string testName = null;
string testResult = null;
string resultLine = null;
List<string> results = new List<string>();
XmlDocument resultsDoc = new XmlDocument();
XmlNode executionNode = null;
XmlNode testMethodNode = null;

// Define the test instance settings
Process testInstance = null;
ProcessStartInfo testInfo = new ProcessStartInfo()
{
    UseShellExecute = false,
    CreateNoWindow = true,
};

// Fetch project list from the disk
List<string> excluded = ConfigurationManager.AppSettings["ExcludedProjects"].Split(',').ToList();
DirectoryInfo assemblyPath = new DirectoryInfo(Assembly.GetExecutingAssembly().Location);
DirectoryInfo[] directories = assemblyPath.Parent.Parent.Parent.Parent.GetDirectories();

// Create a test worklist
List<string> worklist = directories.Where(t => !excluded.Contains(t.Name))
                                    .Select(t => String.Format(ConfigurationManager.AppSettings["MSTestCommand"], t.FullName, t.Name))
                                    .ToList();

// Start test execution
Console.WriteLine("Starting Execution...");
Console.WriteLine();

Console.WriteLine("Results               Top Level Tests");
Console.WriteLine("-------               ---------------");

// Remove any existing run results
if (File.Exists("UnitTests.trx"))
{
    File.Delete("UnitTests.trx");
}

// Run each project in the worklist
foreach (string item in worklist)
{
    testInfo.FileName = item;
    testInstance = Process.Start(testInfo);
    testInstance.WaitForExit();

    if (File.Exists("UnitTests.trx"))
    {
        resultsDoc = new XmlDocument();
        resultsDoc.Load("UnitTests.trx");

        foreach (XmlNode result in resultsDoc.GetElementsByTagName("UnitTestResult"))
        {
            // Get the execution ID for the test
            execId = result.Attributes["executionId"].Value;

            // Find the execution and test method nodes
            executionNode = resultsDoc.GetElementsByTagName("Execution")
                                        .OfType<XmlNode>()
                                        .Where(n => n.Attributes["id"] != null && n.Attributes["id"].Value.Equals(execId))
                                        .First();

            testMethodNode = executionNode.ParentNode
                                            .ChildNodes
                                            .OfType<XmlNode>()
                                            .Where(n => n.Name.Equals("TestMethod"))
                                            .First();

            // Get the class name, test name and result
            className = testMethodNode.Attributes["className"].Value.Split(',')[0];
            testName = result.Attributes["testName"].Value;
            testResult = result.Attributes["outcome"].Value;
            resultLine = String.Format("{0}                {1}.{2}", testResult, className, testName);

            results.Add(resultLine);
            Console.WriteLine(resultLine);
        }

        File.Delete("UnitTests.trx");
    }
}

// Calculate passed / failed test case count
int passed = results.Where(r => r.StartsWith("Passed")).Count();
int failed = results.Where(r => r.StartsWith("Failed")).Count();

// Print the summary
Console.WriteLine();
Console.WriteLine("Summary");
Console.WriteLine("-------");
Console.WriteLine("Test Run {0}", failed > 0 ? "Failed." : "Passed.");
Console.WriteLine();

if (passed > 0)
    Console.WriteLine("\tPassed {0,7}", passed);

if (failed > 0)
    Console.WriteLine("\tFailed {0,7}", failed);

Console.WriteLine("\t--------------");
Console.WriteLine("\tTotal {0,8}", results.Count);

if (failed > 0)
    Environment.Exit(-1);
else
    Environment.Exit(0);

我的App.config文件:

<appSettings>
    <add key="ExcludedProjects" value="UnitTests.Bootstrap,UnitTests.Utils" />
    <add key="MSTestCommand" value="&quot;c:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\MSTest.exe&quot; /testcontainer:&quot;{0}\bin\Release\{1}.dll&quot; /nologo /resultsfile:&quot;UnitTests.trx&quot;" />
</appSettings>

5

MsTest是Visual Studio 2013中“已弃用”的测试框架之一。它仍然用于某些类型的测试,但现在可以执行的许多其他类型的测试都存在于新的敏捷测试运行程序中。现在 SQL 数据库单位测试似乎仍属于“某些类型”,需要通过 MsTest.exe 执行。

最简单的方法是使用 /TestContainer 命令行开关 并为您的测试项目使用命名模式。这样,您就可以快速抓取所有具有特定命名模式的程序集,然后将它们提供给 MsTest。简单的 PowerShell 命令可用于抓取符合您模式的所有文件,然后将它们提供给命令行。

vsmdi 仍然可以在 Visual Studio 2013 中工作,但是编辑器已从工具中删除,并且不再有模板。因此,很难使用。这是微软对 VSDMI 的说法:

注意 测试列表在 Visual Studio 2012 中不再得到完全支持:

  • 您无法创建新的测试列表。
  • 您不能从 Visual Studio 中运行测试列表测试。
  • 如果从 Visual Studio 2010 升级,并且在解决方案中有测试列表,则可以在 Visual Studio 中继续编辑它。
  • 您可以像上面所述那样使用 mstest.exe 从命令行继续运行测试列表。
  • 如果您在构建定义中使用了测试列表,则可以继续使用它。

基本上,他们告诉你停止使用这种技术,并使用 TestCategory 的组合来创建易于执行的测试组。

由于可以为测试容器添加多个参数,因此可以将它们全部分组到一个调用中:

/testcontainer:[file name]        Load a file that contains tests. You can
                                  Specify this option more than once to
                                  load multiple test files.
                                  Examples:
                                    /testcontainer:mytestproject.dll
                                    /testcontainer:loadtest1.loadtest

MsTest /testcontainer:assemblyone.dll /testcontainer:assemblytwo.dll /testcontainer:assembly3.dll

同时运行多个程序集上的MsTest。目前不使用XUnit .NET或NUnit,因为这些工具无法在不切换到新的敏捷测试运行器的情况下合并为一个报告。


是的。但我需要一个报告。 - Nahum
给那些打-1的人,你们能否评论一下原因,这样我就有机会改进答案了吗? - jessehouwing
我担心他们只是为了赏金而战。我以前见过这种行为。 - Nahum
如果您有任何想法,请添加它们。我不是为了赏金而参与其中。我已经足够尊重 :). 如果 /testcontainer 选项对您无效,请解释为什么它无效。当然,存在一些限制。最坏的情况是您需要将不同的输出 XML 合并成一个。 - jessehouwing
“Agile Test Runner”似乎只是TFS的东西?我需要使用TFS而不是像Jenkins这样的工具才能使用它? - red888
1
@red888,敏捷测试运行器实际上是随最新版本的Visual Studio一起提供的vstest.console.exe。请参见:https://msdn.microsoft.com/en-us/library/jj155796.aspx。在TFS XAML构建编辑器中,它被标识为“敏捷测试运行器”。如果需要,您可以直接从Jenkins调用vstest.console.exe。 - jessehouwing

2
为了完整起见,我经常希望将测试作为控制台应用程序运行,因为我发现这样更容易调试……多年来,我创建了一些小的测试助手来帮助自己;我想您也可以很容易地将它们与 CI 解决方案一起使用。

我知道这并不完全是你的问题;然而,既然你正在寻找 CI 解决方案并提到了 Visual Studio,那么这应该能很好地解决它。

只是让你知道,我的小框架比这个大一点,但是缺少的东西很容易添加。基本上我省略了所有日志记录和测试不同程序集在不同应用程序域中(因为可能存在 DLL 冲突和状态)的事情。下面会详细介绍。

需要注意的一件事是,在下面的过程中,我没有捕获任何异常。我的主要重点是使您在故障排除时轻松调试应用程序。我有一个单独的(但类似的)实现用于 CI,基本上在下面的注释点上添加 try/catch。

这种方法只有一个限制:Visual Studio 不会复制您引用的所有程序集;它只会复制您在代码中使用的程序集。解决此问题的简单方法是引入一个从未被调用的方法,它使用您正在测试的 DLL 中的一种类型。这样,您的程序集就会被复制,一切都会很好地运作。

下面是代码:

static class TestHelpers
{
    public static void TestAll(this object o)
    {
        foreach (MethodInfo meth in o.GetType().GetMethods().
            Where((a) => a.GetCustomAttributes(true).
                Any((b) => b.GetType().Name.Contains("TestMethod"))))
        {
            Console.WriteLine();
            Console.WriteLine("--- Testing {0} ---", meth.Name);
            Console.WriteLine();

            // Add exception handling here for your CI solution.
            var del = (Action)meth.CreateDelegate(typeof(Action), o);
            del();

            // NOTE: Don't use meth.Invoke(o, new object[0]); ! It'll eat your exception!

            Console.WriteLine();
        }
    }

    public static void TestAll(this Assembly ass)
    {
        HashSet<AssemblyName> visited = new HashSet<AssemblyName>();
        Stack<Assembly> todo = new Stack<Assembly>();
        todo.Push(ass);

        HandleStack(visited, todo);

    }

    private static void HandleStack(HashSet<AssemblyName> visited, Stack<Assembly> todo)
    {
        while (todo.Count > 0)
        {
            var assembly = todo.Pop();

            // Collect all assemblies that are related
            foreach (var refass in assembly.GetReferencedAssemblies())
            {
                TryAdd(refass, visited, todo);
            }

            foreach (var type in assembly.GetTypes().
                Where((a) => a.GetCustomAttributes(true).
                    Any((b) => b.GetType().Name.Contains("TestClass"))))
            {
                // Add exception handling here for your CI solution.
                var obj = Activator.CreateInstance(type);
                obj.TestAll();
            }
        }
    }

    public static void TestAll()
    {
        HashSet<AssemblyName> visited = new HashSet<AssemblyName>();
        Stack<Assembly> todo = new Stack<Assembly>();

        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            TryAdd(assembly.GetName(), visited, todo);
        }

        HandleStack(visited, todo);
    }

    private static void TryAdd(AssemblyName ass, HashSet<AssemblyName> visited, Stack<Assembly> todo)
    {
        try
        {
            var reference = Assembly.Load(ass);

            if (reference != null &&
                !reference.GlobalAssemblyCache &&           // Ignore GAC
                reference.FullName != null && 
                !reference.FullName.StartsWith("ms") &&     // mscorlib and other microsoft stuff
                !reference.FullName.StartsWith("vshost") && // visual studio host process
                !reference.FullName.StartsWith("System"))   // System libraries
            {
                if (visited.Add(reference.GetName()))       // We don't want to test assemblies twice
                {
                    todo.Push(reference);                   // Queue assembly for processing
                }
            }
        }
        catch
        {
            // Perhaps log something here... I currently don't because I don't care...
        }
    }
}

如何使用此代码:
  1. 您可以简单地调用TestHelpers.TestAll()来测试所有程序集、引用的程序集、间接引用的程序集等。这可能是您在CI中想要做的。
  2. 您可以调用TestHelpers.TestAll(assembly)来测试一个带有所有引用程序集的单个程序集。当您将测试分散到多个程序集中并/或者进行调试时,这可能非常有用。
  3. 您可以调用new MyObject().TestAll()来调用单个对象中的所有测试。这在调试时特别有帮助。

如果您像我一样使用应用程序域,则应为从文件夹动态加载的DLL创建单个应用程序域,并在其上使用TestAll。另外,如果您使用临时文件夹,您可能需要在测试之间清空它。这样,多个测试框架版本和多个测试就不会相互影响。特别是如果您的测试使用状态(例如静态变量),这可能是一个好习惯。有大量关于CreateInstanceAndUnwrap的示例在线可以帮助您解决这个问题。

需要注意的一件事是,我使用委托而不是method.Invoke。这基本上意味着您的异常对象不会被反射吞噬,这意味着您的调试器不会被打断。还要注意,我按名称检查属性,这意味着只要属性名称匹配,就可以与不同的框架一起使用。

希望对您有所帮助


2
我不知道这是否对您有所帮助,但我使用Invoke-MsBuild。它是一个PowerShell模块,应该能够完全满足您的需求。我不知道您是否正在寻找PowerShell解决方案,但它非常好用!
它还有一个姊妹脚本,Invoke-MsTest,可以运行MsTest而不是MsBuild。

0

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