Introduction
Having to create the same stories with the same tasks over and over can be a burden. In our scenario, we create a story for Release Regression Testing and attach several dozen testing tasks, which is very time consuming. This system is setup to copy children that are work items, and attached test cases.
Warning
I cannot be responsible for your setup or your code. The system outlined below works on our current system without issue, but modifying the code or mistakes may cause issues to your TFS system. Use at your own risk as you are any time you work directly against the API. I created the tested system in a Development Environment" clone of our normal TFS system to be safe.
About the Setup
I am not going to worry about the UI and let you pick your own so we will just make the framework, then you can connect to the system however you like, but I will give some pointers. You may want to keep the wrapper in a separate project from the UI so it will be reusable. Doing this allowed me to make a Windows service that automates some tasks like emailing daily test results to the department.
Requirements
You will have to add references to the TFS DLLs. For this, I am using the DLLs from Visual Studio 12. Most of the DLLs can be found in C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\ReferenceAssemblies.
Since we are doing test first you will also need the Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll at C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\PublicAssemblies.
Getting Started
Setting Up A Data Transfer Object
First, create a class called "ClonedWorkItemResult
" that will hold the data for our results. It will consist of two properties of the type WorkItem
, one for the original and one for the clone. To have access to the Work Item class, you need to add the following using
statement.
using Microsoft.TeamFoundation.WorkItemTracking.Client;
Now add the two properties we need.
public WorkItem originalWorkItem {get; set;}
public WorkItem cloneWorkItem {get; set;}
Finish this up with a constructor that inputs and sets both these values:
public ClonedWorkItemResult(WorkItem originalWorkItem, WorkItem cloneWorkItem)
{
this.origialWorkItem = originalWorkItem;
this.cloneWorkItem = cloneWorkItem;
}
Setting Up the Methods
The work items are stored in the Work Item Store, this is in the same namespace we added earlier. The Work Item Store can be retrieved by creating a TFS Team Project Collection instance and getting the service of the type WorkItemStore
. Below is the code for getting the work item store. For this tutorial, I just hard coded the values, but I use a .config file in our web site for the actual values so it can be changed externally.
public static WorkItemStore GetWorkItemStoreService()
{
NetworkCredential networkCredentials =
new NetworkCredential("username", "password","domain");
var tfsUri = https:
var myTFSTeamProjectCollection = new TfsTeamProjectCollection(
TfsTeamProjectCollection.GetFullyQualifiedUriForName(tfsUri),
networkCredentials);
return myTFSTeamProjectCollection.GetService<WorkItemStore>;
}
- Now that we have access to the Work Item Store, we can start working on the code for cloning a work item. For the
main
method, we want to pass in the ID of the original work item and a new title for our cloned item.
public static List<ClonedWorkItemResult> CloneWorkItem(int workItemId, string newTitle) {}
- Now we need to do some work inside of that method, first we need to get an instance of the
WorkItemStore
. We can just call the method we made already.
WorkItemStore wiStore = GetWorkItemStoreService();
- Make a list to store the
ClonedWorkResults
:
List<ClonedWorkItemResult> clonedWorkItemResults = new List<ClonedWorkItemResult>();
- For cloning the work item, we are going to clone each child as well so we need to have a separate method that can be called recursively for each child. Since it is recursive but we need to get the results out of it, we will use a
ref
to the results. Here is the method call we need.
CopyWorkItem(wiStore, workItemId, ref clonedWorkItemResults);
- Before we go build the
CloneWorkItem
method we are going to finish writing the rest of the method calls we need.
The remaining things to do are:
- Set the Cloned Items new title.
- Remove the links in the clones as they are still linked to the originals.
- Save the work items so the clones generate their ID fields.
- Regenerate the links between the new clones.
- Return the results for logging and messaging.
Below is the code we will add and that will finish out the CloneWorkItem
method so we can start working on the methods to do the work outside of it that we are calling.
clonedWorkItemResults.Where
(c => c.originalWorkItem.Id == workItemId).FirstOrDefault().cloneWorkItem.Title = newTitle;
RemoveAllWorkItemLinks(ref clonedWorkItemResults);
SaveAllWorkItems(ref clonedWorkItemResults);
CloneWorkItemLinks(ref clonedWorkItemResults, wiStore);
return clonedWorkItemResults;
- Now to the
CopyWorkItem
method. You can modify this as needed to fit your needs. You may not want to copy test cases, or you may want to include attachments or whatever else, but this will get you going. I will let the comments detail what the code is doing from here on out.
public static void CopyWorkItem(WorkItemStore wiStore, int workItemId,
ref List<ClonedWorkItemResult> clonedWorkItemResults)
{
WorkItem wi = wiStore.GetWorkItem(workItemId);
WorkItem copy = wi.Copy();
foreach (WorkItemLink wiLink in wi.WorkItemLinks)
{
if (wiLink.BaseType == BaseLinkType.WorkItemLink)
{
if (wiLink.LinkTypeEnd.Name == "Child" || wiLink.LinkTypeEnd.Name == "Tested By")
{
CopyWorkItem(wiStore, wiLink.TargetId, ref clonedWorkItemResults);
}
}
}
if (clonedWorkItemResults.Count(c => c.originalWorkItem.Id == workItemId) == 0)
{
clonedWorkItemResults.Add(new ClonedWorkItemResult(wi, copy));
}
}
- Now to Remove the Links to the original work items. This is pretty straight forward. We remove all links and regenerate the ones we want by comparing to the original.
public static void RemoveAllWorkItemLinks(ref List<ClonedWorkItemResult> clonedWorkItemResults)
{
foreach (var clone in clonedWorkItemResults.Select(c => c.cloneWorkItem))
{
clone.WorkItemLinks.Clear();
}
}
- Now to Save the clones. Pretty straight forward here, just save each clone so they generate an ID we can use to regenerate the links.
private static void SaveAllWorkItems(ref List<ClonedWorkItemResult> clonedWorkItemResults)
{
foreach (WorkItem wi in clonedWorkItemResults.Select(c => c.cloneWorkItem))
{
wi.Save();
}
}
- Now to Regenerate the Links on the clones by referencing the original links. A note here, this example does not retain all links, it only retains links that are
WorkItemLinks
, it will need to be modified if you want to retain other kinds of links. Also we ignore links to levels above the item we cloned, you may want to retain the original parent on the clone.
private static void CloneWorkItemLinks
(ref List<ClonedWorkItemResult> clonedWorkItemResults, WorkItemStore wiStore)
{
foreach (ClonedWorkItemResult wiResult in clonedWorkItemResults)
{
foreach (WorkItemLink wiLink in wiResult.originalWorkItem.WorkItemLinks)
{
if (clonedWorkItemResults.Count(c => c.originalWorkItem.Id == wiLink.TargetId) == 0)
{
continue;
}
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())
);
}
wiResult.cloneWorkItem.Save(SaveFlags.MergeLinks);
}
}
Using the Code
Now you can call your code from a Test, a UI project or whatever. Simply call the method and pass in your parameters from whatever input you setup. Here is what you may have from a WPF text box.
var createdItems = CloneWorkItem(txt_WorkItemID.Text, txt_WorkItemNewName.Text);
foreach (var item in createdItems)
{
Console.WriteLine($"Work Item '{item.cloneWorkItem.Id}:
{item.cloneWorkItem.Title}' Cloned from Work Item '{item.originalWorkItem.Id}:
{item.originalWorkItem.Title}'");
}
For a console application, you could pass the Main
method args to the clone method and write to the console.
Ending Comments
This clones the items as they are so cloning a closed story makes a closed story. An easy way of using this is have each team setup a template for the scenario they want, then clone that each time. For our agile process, we create a User Story that has a test case on the story and the following tasks as children.
- Three Amigos/Agile Planning
- Coding
- Write Unit Tests
- Code Review
- Manual Testing
- Coded UI Test Case
- Write Manual Test Case (With presetup steps below)
- ADA Compliance Generic Test Steps
- Visual Testing Step
- Functional Test Generic Steps
Hopefully, you enjoyed this article and it is helpful and in the end, saves you time not having to manually create the same work item setup over and over.
History