如何通过Azure DevOps .NET SDK克隆含测试套件的Test Plan?
解决Azure DevOps Server 2020.1.2测试计划克隆问题
问题分析
你在使用Microsoft.TeamFoundationServer.Client(16.205.1版本)克隆测试计划时遇到三个核心问题:
- 仅克隆测试计划本身,未包含测试套件
- 调用
CloneTestPlanAsync时抛出Newtonsoft.Json.JsonSerializationException,提示缺少opId字段,但克隆操作实际已执行 - 直接调用REST API时,响应始终指向不存在的克隆操作ID 0
这些问题源于Azure DevOps Server 2020.1.2对预览版克隆API的支持有限,以及客户端SDK与服务器端API响应的兼容性差异。
解决方案
1. 绕过序列化异常,获取克隆后的测试计划
服务器返回的响应缺少SDK期望的opId字段,直接使用CloneTestPlanAsync的返回值会触发序列化异常。可以通过捕获异常后,根据计划名称查询克隆结果的方式绕过:
public async Task<TestPlan> CloneTestPlanAsync(int id, CancellationToken cancellationToken = default) { var vssConnection = new VssConnection(new Uri("{PROJECT_COLLECTION_URI}"), {VssCredentials}); TestPlanHttpClient client = await vssConnection.GetClientAsync<TestPlanHttpClient>(cancellationToken); TestPlan sourceTestPlan = await client.GetTestPlanByIdAsync( project: _projectName, planId: id, cancellationToken: cancellationToken); string clonedPlanName = $"{sourceTestPlan.Name} Clone"; // 清理同名计划(如果存在) var existingPlans = await client.GetTestPlansAsync( project: _projectName, name: clonedPlanName, cancellationToken: cancellationToken); foreach (var plan in existingPlans) { await client.DeleteTestPlanAsync(_projectName, plan.Id, cancellationToken: cancellationToken); } var cloneRequestBody = new CloneTestPlanParams() { sourceTestPlan = new SourceTestPlanInfo() { id = id }, destinationTestPlan = new DestinationTestPlanCloneParams() { Name = clonedPlanName, Iteration = sourceTestPlan.Iteration, Area = sourceTestPlan.Area }, cloneOptions = new Microsoft.TeamFoundation.TestManagement.WebApi.CloneOptions() { CopyAllSuites = true, CopyAncestorHierarchy = true, CopyTestCases = true } }; try { // 忽略序列化异常,克隆操作已执行 await client.CloneTestPlanAsync( cloneRequestBody: cloneRequestBody, project: _projectName, cancellationToken: cancellationToken); } catch (Newtonsoft.Json.JsonSerializationException) { // 捕获异常后继续执行 } // 轮询等待克隆完成 TestPlan clonedPlan = null; int retryCount = 0; while (clonedPlan == null && retryCount < 10) { await Task.Delay(2000, cancellationToken); var plans = await client.GetTestPlansAsync( project: _projectName, name: clonedPlanName, cancellationToken: cancellationToken); clonedPlan = plans.FirstOrDefault(); retryCount++; } return clonedPlan; }
2. 手动克隆测试套件(确保套件被复制)
如果上述代码仍未克隆套件,说明Azure DevOps Server 2020.1.2的预览版API不支持CopyAllSuites参数,需要手动克隆每个套件:
在获取到克隆后的测试计划后,添加以下代码:
// 获取源计划的所有子套件 var sourceSuites = await client.GetTestSuitesForPlanAsync( project: _projectName, planId: id, cancellationToken: cancellationToken); foreach (var sourceSuite in sourceSuites) { if (sourceSuite.ParentSuite == null) continue; // 跳过根套件,克隆计划时已创建 var cloneSuiteParams = new CloneTestSuiteParams() { SourceSuite = new SourceTestSuiteInfo() { Id = sourceSuite.Id, PlanId = id }, DestinationSuite = new DestinationTestSuiteCloneParams() { Name = sourceSuite.Name, PlanId = clonedPlan.Id, ParentSuiteId = clonedPlan.RootSuite.Id }, CloneOptions = new CloneOptions() { CopyTestCases = true } }; try { await client.CloneTestSuiteAsync( cloneSuiteParams, project: _projectName, cancellationToken: cancellationToken); } catch (Exception ex) { Console.WriteLine($"克隆套件 {sourceSuite.Name} 失败: {ex.Message}"); } }
3. 适配正确的API版本
Azure DevOps Server 2020.1.2对应的REST API版本为5.1-preview.1,而非6.0-preview.2。直接调用REST API时需指定正确版本:
POST https://{instance}/{collection}/{project}/_apis/testplan/Plans/CloneOperation?api-version=5.1-preview.1
可通过返回的opId轮询克隆状态:
GET https://{instance}/{collection}/{project}/_apis/testplan/Plans/CloneOperation/{opId}?api-version=5.1-preview.1
关键说明
- Azure DevOps Server 2020.1.2的测试计划克隆API处于早期预览阶段,部分参数功能未完全生效
- 手动克隆套件是当前版本下确保套件被复制的可靠方式
- 克隆操作是异步执行的,必须轮询等待完成后再获取结果
内容的提问来源于stack exchange,提问作者Huadara
相关产品推荐
相关产品推荐

