65.9K
CodeProject 正在变化。 阅读更多。
Home

发布外部派生的 TFS 测试用例结果

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2017 年 7 月 28 日

CPOL

4分钟阅读

viewsIcon

10917

本文介绍如何从外部源(例如,您自己开发的自动化测试框架)将自动测试结果发布到 Microsoft 的 Test Manager Infrastructure。

引言

无论是出于在生产环境中进行测试的需要,还是运行与无法在 Microsoft Build 和 Development 环境中运行的工具集成的测试,仍然需要将测试用例结果报告回 Team Foundation Server (TFS)。对于自动化测试而言,这尤其如此,需要自动发布测试用例结果。以下是从与 Microsoft.TestManagment .NET 程序集集成的外部运行的实用程序中处理此问题的一种方法。

注意事项

  • 请自行承担使用风险和(不)便之处.
  • 依赖于 Microsoft Team Explorer。Microsoft Team Explorer (TE) 是免费提供的,有多个版本可匹配您的 Visual Studio 和 TFS 版本。鉴于这些环境的多样性,以及出于对重新分发许可(或缺乏许可)的尊重,依赖的 TE 程序集未包含在内,必须单独获取并配置才能运行。
  • 代码示例密集、不完整。提前为代码风格的疏忽致歉。请根据需要进行美化。为简洁起见,本文代码片段中已省略错误检查和变量获取。

发布测试用例结果

第一步:查找并添加 TestManager 引用

首先,您必须下载并安装 Microsoft Team Explorer。接下来,从您的项目映射对 Microsoft.TeamFoundation.TestManagement.ClientMicrosoft.TeamFoundation.TestManagement.Common 的引用。

您还需要引用 Microsoft.TeamFoundation.ClientMicrosoft.TeamFoundation.CommonMicrosoft.TeamFoundation.WorkItemTracking.ClientMicrosoft.TeamFoundation.WorkItemTracking.Common 程序集。

虽然这会将大约 30 个程序集放入您的输出目录,但 Team Explorer 程序集中有两个重要文件被遗漏,无法直接引用。对此的变通方法是找到 DLL 文件,并将它们作为“Build Action = None”(构建操作 = 无)和“Copy if newer”(如果较新则复制)文件放入您的项目中。

如果您通过 Add|Existing Item,... 项目菜单添加 Microsoft.WITDataStore32.dllMicrosoft.WITDataStore64.dll,那么 DLL 的静态副本将被复制到您的项目源文件文件夹中。我的强迫症让我关闭项目,删除复制的 Microsoft DLL,然后编辑 .csproj 文件将其指向原始目录,例如:

Before:
<ItemGroup>
  <None Include="Microsoft.WITDataStore32.dll">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </None>
  <None Include="Microsoft.WITDataStore64.dll">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </None>
</ItemGroup>
    
After:
<ItemGroup>
  <None Include="..\..\..\..\..\Program Files (x86)\Microsoft Visual Studio\2017\TestPro\
   Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer\Microsoft.WITDataStore32.dll">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </None>
  <None Include="..\..\..\..\..\Program Files (x86)\Microsoft Visual Studio\2017\TestPro\
   Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer\Microsoft.WITDataStore64.dll">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </None>
</ItemGroup>

当您部署您的 'TCResults' 可执行文件时,您必须包含与可执行文件一起被塞入项目构建输出目录的三十几个 DLL 的随行文件。

第二步:获取 TfsTeamProjectCollection 对象

与 TFS .NET 对象集成时的初始步骤是从 TFS 服务器 URI 和项目名称获取 ProjectCollection。如果能找到,此静态例程将返回 ProjectCollection

static TfsTeamProjectCollection GetTeamProjectCollection(Uri TFSServer, string ProjectName)
{
  TfsConfigurationServer configServer = TfsConfigurationServerFactory.GetConfigurationServer(TFSServer);
  ReadOnlyCollection<CatalogNode> projects = configServer.CatalogNode.QueryChildren(new[] 
         { CatalogResourceTypes.ProjectCollection }, false, CatalogQueryOptions.None);

  foreach (CatalogNode pc in projects)
  {
    Guid collection_id = new Guid(pc.Resource.Properties["InstanceId"]);
    ReadOnlyCollection<CatalogNode> teamProjects = pc.QueryChildren(new[] 
        { CatalogResourceTypes.TeamProject }, false, CatalogQueryOptions.None);

    foreach (CatalogNode tp in teamProjects)
    {
      if (string.Compare(ProjectName, tp.Resource.DisplayName, 
                CLU.Option.Collection.CaseSensitivity) == 0)
        return configServer.GetTeamProjectCollection(collection_id);
    }
  }
  return null;
}

第三步:获取要发布到的 TestPlan。

(自行填入加粗的变量值)

TfsTeamProjectCollection teamCollection = 
GetTeamProjectCollection(_tfs_server_uri, _project_name);
ITestManagementService tms = teamCollection.GetService<ITestManagementService>();
ITestManagementTeamProject testProject = tms.GetTeamProject(_project_name);
string wis_command =
    (string.IsNullOrWhiteSpace(_test_plan_name))
    ? " SELECT * FROM TestPlan WHERE [PlanState] = 'Active'"
	: " SELECT * FROM TestPlan WHERE [PlanState] = 'Active' 
	AND [Title] = '" + _test_plan_name + "' ";
ITestPlanCollection testplanCollection = testProject.TestPlans.Query(wis_command);
</code>

解释性插曲

现在情况变得混乱而复杂,其中夹杂着我基于与 TFS 2013 本地服务器的实际工作经验得出的推测。

fluentbytes.com 网站上有一篇由 Marcel de Vries 发布的出色实现指南,介绍了如何通过 TestManagement interfaces 使用 TFS TestCases 和 TestCaseResults。从中,我大致了解了 TestManagement 程序集的 .NET 对象是如何组织的,如下面粗略所示。

但是将新对象写入存储比这个只读图表所暗示的要受到更多限制。下面是相关 TestManagement 对象的接口使用图。

Marcel de Vries 的文章中并未说明 TestCaseResults、TestRuns、TestPoints、TestCases、TestSuites 和 TestPlans 的关系和排序要求。

基本约束

  1. TestCaseResults 只能分配给 TestRunTestPoints。
  2. (根据我的经验)TestPoints 只能在 TestRun 创建后、保存前添加到 TestRun 中。
  3. TestPoints 是 TestCases 与 TestSuite 配置的矩阵交叉。
  4. TestRun 必须在有效的、最好是“Active”(活动)的 TestPlan 下创建。
  5. TestCaseResult 对象在 TestRun 首次保存时实例化。
  6. TestCaseResult 关联的 TestCase 也必须与目标 TestPlan 下的目标 TestSuite 相关联。这暗示了新的 TestRunTestSuite 之间存在同级关系,两者具有相同的父 TestPlan。而 TestCaseTestPoint 之间存在表亲关系。

第四步:间接创建 TestCaseResult 占位符

要使 TeamProjectTestCase(s)(存在结果的)的占位符 TestCaseResult 对象得到实例化,必须验证 TestCase(s) 是否与指定的 TestSuite 相关联。然后,必须创建一个新的 TestRun,添加与 TestCase(s) 相关联的适当 TestPoint(s)。当 TestRun 保存时,占位符 TestCaseResult 对象会被实例化。下面通过 IdAutomatedTestName 字段查询 TestCases。

(自行填入加粗的变量值)

<code>
foreach (ITestPlan testPlan in testplanCollection)
{
  int suite_index = 0;
  ITestSuiteBase testSuite =
      (testPlan.RootSuite.SubSuites.Count > suite_index) 
      ? testPlan.RootSuite.SubSuites[suite_index] : testPlan.RootSuite;
    
  while (testSuite != null)
  {
    if (((ITestSuiteBase2)testSuite).Status.Equals("In Progress")
        &&
        (string.IsNullOrWhiteSpace(_test_suite_name)
        || _test_suite_name.Equals(testSuite.Title))
        &&
        (!string.IsNullOrWhiteSpace(_test_case_id)
        || !string.IsNullOrWhiteSpace(_automated_test_name))
       )
    {
      string testcase_query = null;
      
      if (!string.IsNullOrWhiteSpace(_test_case_id))
        testcase_query = "SELECT * FROM WorkItems WHERE [Work Item Type] = 'Test Case' 
         AND [Id] = '" +  _test_case_id + "'";
      else if (!string.IsNullOrWhiteSpace(_automated_test_name))
        testcase_query = "SELECT * FROM WorkItems WHERE [Work Item Type] = 'Test Case' 
         AND [Microsoft.VSTS.TCM.AutomatedTestName] = '" + _automated_test_name + "'";
    
      foreach (ITestCase test_case in testProject.TestCases.Query(testcase_query))
      {
        ITestRun testRun = testPlan.CreateTestRun(true);
        ITestPointCollection testpointCollection = testPlan.QueryTestPoints
           ("SELECT * FROM [TestPoint] WHERE [TestCaseId] = '" + test_case.Id.ToString() + "'");
        if (testpointCollection.Count == 0)
        {
          testSuite.TestCases.Add(test_case);
          testPlan.Save();
    
          testPlan.Refresh();
          testSuite.Refresh(true);
          testpointCollection = testPlan.QueryTestPoints
            ("SELECT * FROM [TestPoint] WHERE [TestCaseId] = '" + test_case.Id.ToString() + "'");
        }
      
        foreach (ITestPoint test_point in testpointCollection)
        {
          if (test_point.ConfigurationName.Equals(_test_configuration_name))
            testRun.AddTestPoint(test_point, null);
        }
    
        testRun.Title = test_case.Title;
        testRun.Save();</code>

第五步:发布并保存 TestCaseResult

现在占位符 TestCaseResut 对象已实例化,可以设置它们的内部值。

(自行填入加粗的变量值)

        foreach (ITestCaseResult tcr in testRun.QueryResults(false))
        {
          switch (_tcr_outcome)
          {
            case "NONE":
              tcr.Outcome = TestOutcome.None;
              tcr.State = TestResultState.Completed;
              break;
              
            case "PASS":
            case "PASSED":
              tcr.Outcome = TestOutcome.Passed;
              tcr.State = TestResultState.Completed;
              break;
              
            case "FAIL":
            case "FAILED":
              tcr.Outcome = TestOutcome.Failed;
              tcr.State = TestResultState.Completed;
              break;
              
            case "INCONCLUSIVE":
              tcr.Outcome = TestOutcome.Inconclusive;
              tcr.State = TestResultState.Completed;
              break;
              
            case "TIMEOUT":
              tcr.Outcome = TestOutcome.Timeout;
              tcr.State = TestResultState.Completed;
              break;
              
            case "ABORTED":
              tcr.Outcome = TestOutcome.Aborted;
              tcr.State = TestResultState.Completed;
              break;
              
            case "BLOCK":
            case "BLOCKED":
              tcr.Outcome = TestOutcome.Blocked;
              tcr.State = TestResultState.Pending;
              break;
              
            case "NE":
            case "NOTEXECUTED":
              tcr.Outcome = TestOutcome.NotExecuted;
              tcr.State = TestResultState.Pending;
              break;
              
            case "WARNING":
              tcr.Outcome = TestOutcome.Warning;
              tcr.State = TestResultState.Unspecified;
              break;
              
            case "ERROR":
              tcr.Outcome = TestOutcome.Error;
              tcr.State = TestResultState.Unspecified;
              break;
              
            case "NA":
            case "NOT APPLICABLE":
              tcr.Outcome = TestOutcome.NotApplicable;
              tcr.State = TestResultState.Completed;
              break;
              
            case "PAUSED":
              tcr.Outcome = TestOutcome.Paused;
              tcr.State = TestResultState.Paused;
              break;

            case "INPROGRESS":
              tcr.Outcome = TestOutcome.InProgress;
              tcr.State = TestResultState.InProgress;
              break;
              
            case "UNSPECIFIED":
            default:
              tcr.Outcome = TestOutcome.Unspecified;
              tcr.State = TestResultState.Unspecified;
              break;
          }
          tcr.Comment = _tcr_comment;
          
          tcr.Save();
        }
        testRun.QueryResults(false).Save(false);
      }
    }
    testSuite = (testPlan.RootSuite.SubSuites.Count > ++suite_index) ? 
                       testPlan.RootSuite.SubSuites[suite_index] : null;
  }
}

结论

通过能够从外部源发布更详尽的测试用例结果,您的项目和团队将能更好地了解您产品的质量指标。

历史

  • 2017.07.27 - 初始发布
© . All rights reserved.