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

TFS API 克隆包含子项的工作项

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2016 年 8 月 18 日

CPOL

5分钟阅读

viewsIcon

17497

克隆一个 TFS 工作项及其带有链接的子项

引言

一遍又一遍地创建相同的用户故事和相同的任务可能会很繁琐。在我们的场景中,我们为发布回归测试创建一个用户故事,并附加几个几十个测试任务,这非常耗时。本系统旨在复制子项(工作项)和附加的测试用例。

警告

我不对您的设置或代码负责。下面概述的系统在我们当前的系统上运行没有问题,但修改代码或出现错误可能会导致您的 TFS 系统出现问题。您是直接与 API 交互,因此请自行承担风险。为了安全起见,我在我们正常 TFS 系统的“开发环境”克隆中创建了经过测试的系统。

关于设置

我不会关心 UI,让您自己选择,所以我们只构建框架,然后您可以按照自己的方式连接到系统,但我会给出一些建议。您可能希望将包装器保留在与 UI 分开的项目中,以便可以重用。这样做使我能够创建一个 Windows 服务,自动执行一些任务,例如每天将测试结果发送电子邮件给部门。

要求

您需要添加对 TFS DLL 的引用。为此,我使用的是 Visual Studio 12 的 DLL。大多数 DLL 可以在 C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\ReferenceAssemblies 中找到。

由于我们是先进行测试,因此您还需要 Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll,它位于 C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\PublicAssemblies

入门

设置数据传输对象

首先,创建一个名为“ClonedWorkItemResult的类,它将保存我们结果的数据。它将包含两个 WorkItem 类型的属性,一个用于原始项,一个用于克隆项。要访问 Work Item 类,您需要添加以下 using 语句。

using Microsoft.TeamFoundation.WorkItemTracking.Client;

现在添加我们需要的两个属性。

public WorkItem originalWorkItem {get; set;}
public WorkItem cloneWorkItem {get; set;}

用一个接受并设置这两个值的构造函数来完成此操作。

public ClonedWorkItemResult(WorkItem originalWorkItem, WorkItem cloneWorkItem)
{
    this.origialWorkItem = originalWorkItem;
    this.cloneWorkItem = cloneWorkItem;
}

设置方法

工作项存储在 **工作项存储 (Work Item Store)** 中,它与我们之前添加的命名空间相同。可以通过创建 TFS Team Project Collection 实例并获取 WorkItemStore 类型的服务来检索工作项存储。下面是获取工作项存储的代码。本教程中,我只是硬编码了值,但在我们的网站中,我使用了 .config 文件来存储实际值,以便可以从外部更改。

public static WorkItemStore GetWorkItemStoreService()
{
     NetworkCredential networkCredentials = 
     new NetworkCredential("username", "password","domain");

     var tfsUri = https://tfs.company.com/tfs";

     var myTFSTeamProjectCollection = new TfsTeamProjectCollection(
     TfsTeamProjectCollection.GetFullyQualifiedUriForName(tfsUri), 
     networkCredentials);
 
     return myTFSTeamProjectCollection.GetService<WorkItemStore>;
}
  1. 现在我们已经可以访问工作项存储,我们可以开始编写**克隆工作项**的代码了。对于 main 方法,我们希望传入原始工作项的 ID 和克隆项的新标题。
    public static List<ClonedWorkItemResult> CloneWorkItem(int workItemId, string newTitle) {}
  2. 现在我们需要在该方法中做一些工作,首先我们需要获取 WorkItemStore 的实例。我们可以调用我们已经写好的方法。
    WorkItemStore wiStore = GetWorkItemStoreService();
  3. 创建一个列表来**存储** ClonedWorkResults
    List<ClonedWorkItemResult> clonedWorkItemResults = new List<ClonedWorkItemResult>();
  4. 为了克隆工作项,我们还将**克隆每个子项**,因此我们需要一个可以为每个子项递归调用的单独方法。由于它是递归的,但我们需要从中获取结果,因此我们将使用 `ref` 来引用结果。这是我们需要调用的方法。
    CopyWorkItem(wiStore, workItemId, ref clonedWorkItemResults);
  5. 在开始构建 CloneWorkItem 方法之前,我们将完成编写我们需要的其余方法调用。

    剩余的工作包括:

    1. 设置克隆项的新标题。
    2. 删除克隆项中的链接,因为它们仍然链接到原始项。
    3. 保存工作项,以便克隆项生成其 ID 字段。
    4. 重新生成新克隆项之间的链接。
    5. 返回结果以进行日志记录和消息传递。

    下面是我们将在 CloneWorkItem 方法中添加的代码,它将完成该方法,以便我们可以开始处理我们正在调用的外部方法。

    //Set the title for the clone
    clonedWorkItemResults.Where
    (c => c.originalWorkItem.Id == workItemId).FirstOrDefault().cloneWorkItem.Title = newTitle;
    
    //Remove the links to the original work items for the clones
    RemoveAllWorkItemLinks(ref clonedWorkItemResults);
    
    //Save the clone work items to generate Ids
    SaveAllWorkItems(ref clonedWorkItemResults);
    
    //Rebuild the links between the clones as the originals had
    CloneWorkItemLinks(ref clonedWorkItemResults, wiStore);
    
    //Return the results for logging and messaging.
    return clonedWorkItemResults;
  6. 现在是 CopyWorkItem 方法。您可以根据需要进行修改。您可能不想复制测试用例,或者您可能希望包含附件或其他内容,但这将使您入门。从现在开始,我将让注释详细说明代码的作用。
    public static void CopyWorkItem(WorkItemStore wiStore, int workItemId, 
    ref List<ClonedWorkItemResult> clonedWorkItemResults)
    {
           //Get the work Item to copy
           WorkItem wi = wiStore.GetWorkItem(workItemId);
            
           //Copy the work item 
           WorkItem copy = wi.Copy();  
    
           //Iterate through all of the links we have 
           foreach (WorkItemLink wiLink in wi.WorkItemLinks) 
           { 
             //make sure the link is a Work Item link, as we dont want to worry about attachments etc.. 
             if (wiLink.BaseType == BaseLinkType.WorkItemLink) 
             { 
               //We only want to copy the children and the Tested by which is the test case, no parents. 
               if (wiLink.LinkTypeEnd.Name == "Child" || wiLink.LinkTypeEnd.Name == "Tested By") 
               { 
                 //There is a child or test case so we need to copy that child as well. 
                 //We are calling the same method we are in so it is recursive for every child. 
                 CopyWorkItem(wiStore, wiLink.TargetId, ref clonedWorkItemResults); 
               } 
             } 
           } 
           //Failsafe to make sure we do not add a work item that is already  
           if (clonedWorkItemResults.Count(c => c.originalWorkItem.Id == workItemId) == 0) 
           { 
             //Add the clone to the results list 
             clonedWorkItemResults.Add(new ClonedWorkItemResult(wi, copy)); 
           } 
         } 
  7. 现在来**删除到原始工作项的链接**。这很简单。我们删除所有链接,然后通过与原始项进行比较来重新生成我们想要的链接。
    public static void RemoveAllWorkItemLinks(ref List<ClonedWorkItemResult> clonedWorkItemResults)
     {
         foreach (var clone in clonedWorkItemResults.Select(c => c.cloneWorkItem))
         {
             clone.WorkItemLinks.Clear();
         }
     }
  8. 现在来**保存克隆项**。这里很简单,只需保存每个克隆项,以便它们生成一个我们可以用来重新生成链接的 ID。
    private static void SaveAllWorkItems(ref List<ClonedWorkItemResult> clonedWorkItemResults)
     {
         foreach (WorkItem wi in clonedWorkItemResults.Select(c => c.cloneWorkItem))
         {
              wi.Save();
         }
     }
  9. 现在通过引用原始链接来**重新生成**克隆项上的**链接**。在此说明一点,此示例不保留所有链接,它只保留 `WorkItemLinks` 类型的链接,如果您想保留其他类型的链接,则需要进行修改。此外,我们忽略了指向克隆项之上级别的链接,您可能希望在克隆项上保留原始父项。
    private static void CloneWorkItemLinks
    (ref List<ClonedWorkItemResult> clonedWorkItemResults, WorkItemStore wiStore)
     {            
         //iterate through the results
         foreach (ClonedWorkItemResult wiResult in clonedWorkItemResults)
         {
             //iterate through the Work Item Links on the original work item.
             foreach (WorkItemLink wiLink in wiResult.originalWorkItem.WorkItemLinks)
         {
              //If we did not clone the Work item go to the next, 
              //i.e., skip the parent epic if we cloned a story
              if (clonedWorkItemResults.Count(c => c.originalWorkItem.Id == wiLink.TargetId) == 0)
              {
                  continue;
              }
     
             //Create a new link of the same type as the original
             wiResult.cloneWorkItem.WorkItemLinks.Add(new WorkItemLink
             (
                  wiStore.WorkItemLinkTypes.LinkTypeEnds[wiLink.LinkTypeEnd.Name],
                  wiResult.cloneWorkItem.Id,
                  clonedWorkItemResults.Where(
                      c => c.originalWorkItem.Id == wiLink.TargetId).Select(
                      c => c.cloneWorkItem.Id).FirstOrDefault())
             );
         }
    
         //Save the clones
          wiResult.cloneWorkItem.Save(SaveFlags.MergeLinks);
         }
     }

Using the Code

现在您可以从测试、UI 项目或其他任何地方调用您的代码。只需调用该方法,然后从您设置的任何输入中传入参数。以下是您可能从 WPF 文本框中获得的内容。

var createdItems = CloneWorkItem(txt_WorkItemID.Text, txt_WorkItemNewName.Text);

foreach (var item in createdItems)
 {
      //If not using c#6 change $ to string.format and move the variables.
      Console.WriteLine($"Work Item '{item.cloneWorkItem.Id}: 
        {item.cloneWorkItem.Title}' Cloned from Work Item '{item.originalWorkItem.Id}: 
        {item.originalWorkItem.Title}'");
 }

对于控制台应用程序,您可以将 `Main` 方法的参数传递给克隆方法,并将结果写入控制台。

结束语

这会按原样克隆项,因此克隆已关闭的用户故事会生成一个已关闭的用户故事。使用此功能的一个简单方法是让每个团队为他们想要的场景设置一个模板,然后每次都克隆该模板。对于我们的敏捷流程,我们创建一个用户故事,该用户故事在用户故事中有一个测试用例,以及以下作为子项的任务。

  • 三人组/敏捷规划
  • 编码
  • 编写单元测试
  • 代码审查
  • 手动测试
  • 编码 UI 测试用例
  • 编写手动测试用例(具有下面的预设步骤)
    • ADA 合规性通用测试步骤
    • 视觉测试步骤
    • 功能测试通用步骤

希望您喜欢这篇文章,它有所帮助,最终可以节省您不必一遍又一遍地手动创建相同工作项设置的时间。

历史

  • 原始帖子
© . All rights reserved.