워크플로를 원활하게 파트 13: 테스트 관리 시나리오 지원

Jessie Cho

‘워크플로를 원활하게’ 시리즈에 다시 오신 것을 환영합니다! QA 팀에서 활용할 수 있는 YouTrack 기능에 대해 커뮤니티로부터 몇 가지 논의 요청을 받았습니다. 오늘은 YouTrack에서 테스트 관리 시나리오(TMS)를 관리하는 방법을 살펴보겠습니다. 이 문서에서는 YouTrack 자체 내에서 테스트 관리 기능을 관리하여 타사 솔루션을 사용하지 않아도 되도록 YouTrack을 설정하는 방법을 보여드릴 것입니다. 이러한 접근 방식은 라이선스 비용을 절감하고 테스트 관리 프로세스 흐름을 보다 원활하게 만드는 데 도움이 됩니다.

이 문서는 테스트 관리자 및 YouTrack 워크플로에 관심이 있는 모든 사용자를 대상으로 합니다. 프로젝트 및 이슈 설정에 대한 권장 사항, 테스트 관리 시나리오를 구현 방법에 대한 설명 등 YouTrack에서 테스트 프로세스를 구축하는 방법에 대한 실용적인 예를 찾을 수 있을 것입니다.

또한 테스트 관리 프로세스를 자동화하는 데 사용할 수 있는 즉시 이용 가능한 워크플로 코드 블록 목록도 준비했습니다. 이러한 코드 블록을 사용하면 테스트 케이스를 특정 테스트 실행에 연결하고, 테스트 실행을 복제하고, 다음 테스트에 대한 시스템 제안을 표시하는 등의 작업을 쉽게 수행할 수 있습니다.

YouTrack의 테스트 관리 프로젝트 설정

Test Case, Test Suite(테스트 케이스 세트), Test Run(특정 테스트 주기에 할당된 테스트 케이스 또는 테스트 스위트 세트) 및 특정 테스트 실행에 지정된 Test Case Executions와 같이 테스트 관리 프로젝트에 테스트 관리별 이슈 유형을 사용할 수 있습니다. 해당 TMS 프로젝트에 필요한 이슈 유형을 구성해야 합니다. 테스트 관리 작업 간의 연결 및 관련성을 수립하기 위한 세 가지 유형의 이슈 링크가 있습니다.

  • 표준 ‘parent-subtasks’는 Test Run 및 Test Case Executions는 물론 Test Suite 및 Test Case 관계를 관리합니다.
  • 사용자 정의 ‘Execution(실행)’은 테스트 케이스 및 테스트 케이스 실행 관계를 관리합니다.
  • 사용자 정의 ‘관련 버그’는 실패한 테스트 및 할당된 버그 관계를 관리합니다.

테스트 요구 사항은 텍스트 필드의 이슈에 연결된 자료로 관리될 수 있습니다.
또한 테스트 관리 작업을 위해 테스트 모드, 카테고리, 테스트 플로애플리케이션 등의 사용자 정의 필드와 사전 정의된 값(예: 사전 정의된 테스트 상태)을 설정할 수 있습니다. 하나의 이슈 유형에 대해 특정하게 정보를 표시해야 하는 경우, 조건부 필드를 사용할 수도 있습니다.

‘Test Case’ 및 ‘Test Run’ 이슈 유형에 대한 사전 정의된 필드 모음은 비즈니스 요구에 맞게 조정할 수 있습니다.

Test Run 및 Test Execution 설정

예를 들어, 특정 제품 버전을 테스트하려는 경우, Test run을 만들고 관련 Test Suite와 Test Case를 여기에 할당해야 합니다. 이를 위해 따라야 할 단계는 다음과 같습니다.

  • ‘Test Run’ 이슈 유형으로 이슈를 생성합니다.
  • ‘Assigned test case and test suite’ 사용자 정의 링크 유형을 사용하여 이슈에 연결합니다.
  • 팝업 창에서 Test run에 포함할 Test Case를 지정합니다.

‘Populate Test Run’ 워크플로의 단계를 통해 QA 팀의 시간과 노력을 절약할 수 있습니다.

  • 선택한 모든 Test Suite 및 Test case의 사본이 시스템에 생성됩니다. 모든 이슈에는 ‘Test Case Execution’ 이슈 유형이 있습니다.
  • ‘Execution’ 링크 유형은 Test Case Executions를 선택한 Test Case에 연결합니다.
  • 새로 생성된 이슈는 상위 이슈 및 관련 하위 작업으로 Test Run과 연결됩니다.

작동 방식은 다음과 같습니다.

코드 블록 확장하기

var entities = require('@jetbrains/youtrack-scripting-api/entities');
var workflow = require('@jetbrains/youtrack-scripting-api/workflow');

exports.rule = entities.Issue.onChange({
  title: 'Populate-test-run',
  guard: function(ctx) {
    var issue = ctx.issue;
    return !issue.isChanged('project') && issue.Type && (issue.Type.name == ctx.Type.TestRun.name) && issue.links[ctx.Execution.outward].added.isNotEmpty() && issue.isReported;
  },
  action: function(ctx) {
    var issue = ctx.issue;
    var totalTestRuns = issue.links[ctx.Execution.outward].added.size;
    issue.links[ctx.Execution.outward].added.forEach(function(TestCase) {
      TestCase.links[ctx.Execution.inward].delete(issue);

      var TestCaseRun = TestCase.copy();
      TestCaseRun.Type = ctx.Type.TestExecution.name;
      TestCaseRun.Status = ctx.Status.NoRun.name;
      Object.keys(TestCaseRun.links).forEach(function(linkType) {
       if (!TestCaseRun.links[linkType])
        return;
         TestCaseRun.links[linkType].clear();
      });
      TestCaseRun.summary = "[TEST_CASE_EXECUTION" + "] [" + TestCaseRun.summary + "]";

      TestCaseRun.links[ctx.Subtask.inward].add(issue);
      issue.links[ctx.Subtask.outward].add(TestCaseRun);
      TestCaseRun.links[ctx.Execution.outward].add(TestCase);
    });
    issue.fields['Total number of test cases'] = totalTestRuns;
  },
  requirements: {
    Execution: {
      type: entities.IssueLinkPrototype,
      name: 'Execution',
      inward: 'Execution',
      outward: 'Assigned test case or test suite'
    },
    Subtask: {
      type: entities.IssueLinkPrototype,
      name: 'Subtask',
      inward: 'parent for',
      outward: 'subtask of'
    },
    Type: {
      type: entities.EnumField.fieldType,
      TestExecution: {
        name: "Test Case Execution"
      },
      TestRun: {
        name: "Test Run"
      },
      TestCase: {
        name: "Test Case"
      },
      TestSuite: {
        name: "Test Suite"
      }
    },
    Total: {
      type: entities.Field.integerType,
      name: 'Total number of test cases'
    },
    TotalFailed: {
      type: entities.Field.integerType,
      name: 'Number of failed test cases'
    },
    TotalPassed: {
      type: entities.Field.integerType,
      name: 'Number of passed test cases'
    },
    Status: {
      type: entities.EnumField.fieldType,
      InProgress: {
        name: 'In Progress'
      },
      Passed: {
        name: 'Passed'
      },
      Failed: {
        name: 'Failed'
      },
      NoRun: {
        name: 'No Run'
      },
    },
  }
});

테스트 활동 관리

Test Run이 완전히 설정되면 테스트 엔지니어가 테스트를 시작할 수 있습니다. 기본적으로, ‘테스트 케이스 수행’ 및 ‘테스트 실행’ 유형의 모든 이슈는 ‘실행 없음’ 상태입니다.

테스트 사이를 전환하는 방법

테스트 세트로 작업할 때 한 테스트에서 다른 테스트로 전환하는 방법에는 두 가지가 있습니다.

  1. 이슈 목록 페이지에서 테스트 상태를 수동으로 변경합니다.
  2. 테스트를 열고 완료되지 않은 다음 테스트로 전환합니다. 이 방식으로 테스트를 전환하고자 할 때는, 다음 테스트를 실행하도록 제안하는 유용한 팝업이 나타납니다.
    다음의 액션은 자동화될 수 있습니다.

    • ‘No Run’ 상태이고 동일한 테스트 실행에 속하는 테스트가 있는지 확인합니다.
    • 사용 가능한 다음 테스트의 URL이 있는 메시지를 표시합니다(있는 경우).

      ApplyingCommandForSubsystem

    이를 구현하는 데 아래 코드를 사용할 수 있습니다.

    코드 블록 확장하기

     action: function(ctx) {
      var issue = ctx.issue;
      if (!issue.links['subtask of'].isEmpty) {
                  var parent = issue.links['subtask of'].first();
                  var TestRunList = parent.links[ctx.Subtask.outward];
                  var resultSet = null;
                  var isPassing = true;
                  TestRunList.forEach(function(v) {
                    if (v.Status.name == ctx.Status.Failed.name) {
                      isPassing = false;
                    } else if ((v.Status.name == ctx.Status.InProgress.name) && (v.id !== issue.id)) {
                      resultSet = v;
                    }
                  });
                  if (resultSet) {
                    var otherIssueLink = ' ' + resultSet.id + '';
                    var message = 'Switch to next open test in current Test Run' + otherIssueLink + '.';
                    workflow.message(message);
        }
    }
    }
    

테스트 상태

한 테스트에서 다른 테스트로 전환할 때 테스트 엔지니어는 테스트가 ‘Passed(통과)’된 것으로 나타내거나, 테스트 상태를 ‘Failed(실패)'(Test case 수행 중에 버그가 확인된 경우)로 변경할 수 있습니다. 이 프로젝트에 대해 처음에 구성한 YouTrack 설정에 따라 사전 정의된 몇 가지 테스트 실행 상태가 존재합니다.

  • Failing(실패): 연결된 테스트 중 하나 이상이 ‘No run’ 상태이고, 연결된 테스트 중 하나 이상이 ‘Failed’ 상태임.
  • Passing(통과): 연결된 테스트 중 하나 이상이 ‘No run’ 상태이고, 상태가 ‘Failed’인 연결된 테스트가 없음.
  • Failed(실패): ‘No Run’ 상태인 연결된 테스트가 없고, 하나 이상의 연결된 테스트가 ‘Failed’ 상태임.
  • Passed(통과함): 연결된 테스트에 ‘No Run’ 또는 ‘Failed’ 상태가 없음.

위에 나열된 상태는 QA 팀이 전체 테스트 주기 동안 테스트 진행 상황을 모니터링하는 데 도움이 될 수 있습니다. Test Run 상태는 연결된 테스트에 따라서만 결정되며 수동으로 변경해서는 안된다는 점에 유의해야 합니다. Test Run 상태를 결정하는 프로세스는 state-machines per issue type(이슈 유형별 상태 시스템) 워크플로를 사용하여 구현됩니다. 테스트 간 전환을 위한 코드도 워크플로에 통합됩니다.

코드 블록 확장하기


var entities = require('@jetbrains/youtrack-scripting-api/entities');
var workflow = require('@jetbrains/youtrack-scripting-api/workflow');

exports.rule = entities.Issue.stateMachine({
  title: 'status-management',
  stateFieldName: 'Status',
  typeFieldName: 'Type',
  defaultMachine: {
    'No Run': {
      initial: true,
      transitions: {
        'Failed': {
          targetState: 'Failed',
          action: function(ctx) {
            var issue = ctx.issue;
            if (!issue.links['subtask of'].isEmpty) {
              var parent = issue.links['subtask of'].first();
              var TestRunList = parent.links[ctx.Subtask.outward];
              var resultSet = null;
              var isPassing = true;
              TestRunList.forEach(function(v) {
                if (v.Status.name == ctx.Status.Failed.name) {
                  isPassing = false;
                } else if ((v.Status.name == ctx.Status.InProgress.name) && (v.id !== issue.id)) {
                  resultSet = v;
                }
              });
              if (resultSet) {
                var otherIssueLink = ' ' + resultSet.id + '';
                var message = 'Switch to next open test in current Test Run' + otherIssueLink + '.';
                workflow.message(message);
                // Updating Test Run Status 
                parent.fields["Status"] = ctx.Status.Failing;
              } else {
                parent.fields["Status"] = ctx.Status.Failed;
              }
            }
          }
        },
        'Passed': {
          guard: function(ctx) {
            var issue = ctx.issue;
            return !issue.isChanged('project') && !issue.becomesReported && issue.isReported && (issue.Type.name == ctx.Type.TestExecution.name);
          },
          targetState: 'Passed',
          action: function(ctx) {
            var issue = ctx.issue;
            if (!issue.links['subtask of'].isEmpty) {
              var parent = issue.links['subtask of'].first();
              var TestRunList = parent.links[ctx.Subtask.outward];
              var resultSet = null;
              var isPassing = true;
              TestRunList.forEach(function(v) {
                if (v.Status.name == ctx.Status.Failed.name) {
                  isPassing = false;
                } else if ((v.Status.name == ctx.Status.InProgress.name) && (v.id !== issue.id)) {
                  resultSet = v;
                }
              });
              if (resultSet) {
                var otherIssueLink = ' ' + resultSet.id + '';
                var message = 'Switch to next open test in current Test Run' + otherIssueLink + '.';
                workflow.message(message);
                parent.fields["Status"] = (isPassing) ? ctx.Status.Passing : ctx.Status.Failing;
              } else {
                parent.fields["Status"] = (isPassing) ? ctx.Status.Passed : ctx.Status.Failed;
              }
            }
          }
        }
      }
    },
    Passed: {
      transitions: {
        'Failed': {
          guard: function(ctx) {
            var issue = ctx.issue;
            return !issue.isChanged('project') && !issue.becomesReported && issue.isReported && (issue.Type.name == ctx.Type.TestExecution.name);
          },
          targetState: 'Failed',
          action: function(ctx) {
            var issue = ctx.issue;
            if (!issue.links['subtask of'].isEmpty) {
             var parent = issue.links['subtask of'].first();
              var TestRunList = parent.links[ctx.Subtask.outward];
              var resultSet = null;
              TestRunList.forEach(function(v) {
                if (v.Status.name == ctx.Status.Failed.name) {
                } else if ((v.Status.name == ctx.Status.InProgress.name) && (v.id !== issue.id)) {
                  resultSet = v;
                }
              });
              if (resultSet) {
                var otherIssueLink = ' ' + resultSet.id + '';
                var message = 'Switch to next open test in current Test Run' + otherIssueLink + '.';
                workflow.message(message);
                parent.fields["Status"] = ctx.Status.Failing;
              } else {
                parent.Status = ctx.Status.Failed;
              }
            }
          }
        },
        'No Run': {
          guard: function(ctx) {
            var issue = ctx.issue;
            return !issue.isChanged('project') && !issue.becomesReported && issue.isReported && (issue.Type.name == ctx.Type.TestExecution.name);
          },
          targetState: 'No Run',
          action: function(ctx) {
            var issue = ctx.issue;
            if (!issue.links['subtask of'].isEmpty) {
              var parent = issue.links['subtask of'].first();
              var TestRunList = parent.links[ctx.Subtask.outward];
              var ActiveTestRun = false;
              var isPassing = true;
              TestRunList.forEach(function(v) {
                if (v.Status.name == ctx.Status.Failed.name) {
                  isPassing = false;
                  ActiveTestRun = true;
                } else if ((v.Status.name == ctx.Status.Passed.name) && (v.id !== issue.id)) {
                  ActiveTestRun = true;
                }
              });
              if (ActiveTestRun) {
                parent.fields["Status"] = (isPassing) ? ctx.Status.Passing : ctx.Status.Failing;
              } else parent.fields["Status"] = ctx.Status.InProgress;
            }
          }
        }
      }
    },
    Failed: {
      transitions: {
        'Passed': {
          guard: function(ctx) {
            var issue = ctx.issue;
            return !issue.isChanged('project') && !issue.becomesReported && issue.isReported && (issue.Type.name == ctx.Type.TestExecution.name);
          },
          targetState: 'Passed',
          action: function(ctx) {
            var issue = ctx.issue;
            if (!issue.links['subtask of'].isEmpty) {
              var parent = issue.links['subtask of'].first();
              var TestRunList = parent.links[ctx.Subtask.outward];
              var resultSet = null;
              var isPassing = true;
              TestRunList.forEach(function(v) {
                if ((v.Status.name == ctx.Status.Failed.name) && (v.id !== issue.id)) {
                  isPassing = false;
                } else if ((v.Status.name == ctx.Status.InProgress.name) && (v.id !== issue.id)) {
                  resultSet = v;
                }
              });
              if (resultSet) {
                var otherIssueLink = ' ' + resultSet.id + '';
                var message = 'Switch to next open test in current Test Run' + otherIssueLink + '.';
                workflow.message(message);

                parent.fields["Status"] = (isPassing) ? ctx.Status.Passing : ctx.Status.Failing;
              } else {
                parent.fields["Status"] = (isPassing) ? ctx.Status.Passed : ctx.Status.Failed;
              }
            }
          }
        },
        'No Run': {
          guard: function(ctx) {
            var issue = ctx.issue;
            return !issue.isChanged('project') && !issue.becomesReported && issue.isReported && (issue.Type.name == ctx.Type.TestExecution.name);
          },
          targetState: 'No Run',
          action: function(ctx) {
            var issue = ctx.issue;
            if (!issue.links['subtask of'].isEmpty) {
              var parent = issue.links['subtask of'].first();
              var TestRunList = parent.links[ctx.Subtask.outward];
              var ActiveTestRun = false;
              var isPassing = true;
              TestRunList.forEach(function(v) {
                if ((v.Status.name == ctx.Status.Failed.name) && (v.id !== issue.id)) {
                  isPassing = false;
                  ActiveTestRun = true;
                } else if ((v.Status.name == ctx.Status.Passed.name) && (v.id !== issue.id)) {
                  ActiveTestRun = true;
                }
              });
              if (ActiveTestRun) {
                parent.fields["Status"] = (isPassing) ? ctx.Status.Passing : ctx.Status.Failing;
              } else parent.fields["Status"] = ctx.Status.InProgress;
            }
          }
        }
      }
    }
  },
  alternativeMachines: {
    'Test Run': {
      'No Run': {
        initial: true,
        transitions: {
          'Failing': {
            targetState: 'Failing',
            action: function(ctx) {
           /* Add actions. */
            }
          },
          'Failed': {
            targetState: 'Failed',
            action: function(ctx) {
         /* Add actions. */
            }
          },
          'Passing': {
            targetState: 'Passing',
            action: function(ctx) {
          /* Add actions. */
            }
          },
          'Passed': {
            targetState: 'Passed',
            action: function(ctx) {
           /* Add actions. */
            }
          }
        }
      },
      Failing: {
        transitions: {
          'Passing': {
            targetState: 'Passing',
            action: function(ctx) {
           /* Add actions . */
            }
          },
          'Passed': {
            targetState: 'Passed',
            action: function(ctx) {
          /* Add actions. */
            }
          },
          'Failed': {
            targetState: 'Failed',
            action: function(ctx) {
           /* Add actions. */
            }
          }
        }
      },
      Passing: {
        transitions: {
          'Failing': {
            targetState: 'Passing',
            action: function(ctx) {
              workflow.check(false, workflow.i18n('Test Run has-read-only status which is defined based on assigned tests statuses'));
            }
          },
          'Passed': {
            targetState: 'Passed',
            action: function(ctx) {
           /* Add actions. */
            }
          },
          'Failed': {
            targetState: 'Failed',
            action: function(ctx) {
           /* Add actions. */
            }
          }
        }
      },
      Failed: {
        transitions: {
          'Passing': {
            targetState: 'Passing',
            action: function(ctx) {
           /* Add actions. */
            }
          },
          'Passed': {
            targetState: 'Passed',
            action: function(ctx) {
           /* Add actions. */
            }
          },
          'Failing': {
            targetState: 'Failed',
            action: function(ctx) {
           /* Add actions. */
            }
          }
        }
      },
      Passed: {
        transitions: {
          'Passing': {
            targetState: 'Passing',
            action: function(ctx) {
           /* Add actions. */
            }
          },
          'Failed': {
            targetState: 'Passed',
            action: function(ctx) {
          /* Add actions. */
            },
          },
          'Failing': {
            targetState: 'Failed',
            action: function(ctx) {
           /* Add actions. */
            }
          }
        }
      }
    }
  },
  requirements: {
    Assignee: {
      type: entities.User.fieldType
    },
    Status: {
      type: entities.EnumField.fieldType,
      InProgress: {
        name: 'No Run'
      },
      Failing: {
        name: 'Failing'
      },
      Passing: {
        name: 'Passing'
      },
      Passed: {
        name: 'Passed'
      },
      Failed: {
        name: 'Failed'
      },
    },
    Type: {
      type: entities.EnumField.fieldType,
      TestRun: {
        name: "Test Run"
      },
      TestExecution: {
        name: "Test Case Execution"
      }
    },
    Subtask: {
      type: entities.IssueLinkPrototype,
      name: 'Subtask',
      inward: 'subtask of',
      outward: 'parent for'
    },
  }
});
 

테스트 통계

주요 테스트 주기 통계에 관심이 있을 수도 있습니다. 아래의 데모에서는 다음과 같은 메트릭스를 통합하였습니다.

  • 특정 Test Run에 할당된 총 테스트 수.
  • ‘Passed(통과됨)’ 상태의 테스트 수.
  • ‘Failed(실패함)’ 상태의 테스트 수.


아래 코드 덕분에 테스트 상태 변경, Test Run에 새 테스트 할당 및 Test Run에서 테스트 제거와 같은 변경이 발생하면 모든 메트릭이 업데이트됩니다.

코드 블록 확장하기

  exports.calculateStatuses = function(parent) {
  var totalTR = 0;
  var totalFailed = 0;
  var totalPassed = 0;
  if (!parent.links['parent for']) {
    return;
  } else {
    parent.links['parent for'].forEach(function(tr) {
      totalTR++;
      if (tr.Status.name == 'Passed') {
        totalFailed++;
      }
      if (tr.Status.name == 'Failed') {
        totalPassed++;
      }
    });
    parent.fields['Total number of test cases'] = totalTR;
    parent.fields['Number of passed test cases'] = totalPassed;
    parent.fields['Number of failed test cases'] = totalFailed;
    return true;
  }
};
exports.resetStatuses = function(testRun, testRunCopy) {
   testRunCopy.fields['Total number of test cases'] = testRun.fields['Total number of test cases'];
   testRunCopy.fields['Number of passed test cases'] = 0;
   testRunCopy.fields['Number of failed test cases'] = 0;
  return true;
};

이 경우에 이 코드는 ‘Update stats when links are adjusted’(링크가 조정되면 통계 업데이트)’, ‘Switch to the next test case(다음 테스트 케이스로 전환)’ 등과 같은 여러 워크플로 규칙에 따라 트리거됩니다. 특정 요구 사항에 맞는 메트릭 세트를 사용하려는 경우, 필요한 사용자 정의 필드를 추가하고 워크플로 논리를 조정할 수 있습니다.

이슈 트래커 기능

실패한 테스트에서 발견된 버그를 해결하는 별도의 작업을 만들고, 이 작업을 관련 Test Case Execution을 식별하는 이슈에 연결할 수도 있습니다. 사용자 정의 이슈 링크 유형을 사용하여 실패한 테스트를 관련 버그와 연결하거나, 버그에 대한 참조가 포함된 사용자 정의 텍스트 필드를 사용할 수 있습니다. 버그를 추적하기 위해 별도의 YouTrack 프로젝트를 구성하는 것이 더 쉬울 수 있습니다.

가능한 기능 개선

추가적인 비즈니스 요구 사항을 충족하도록 YouTrack의 기능을 확장할 수 있습니다. 고려할 수 있는 몇 가지 옵션이 있습니다.

기존 Test Run 복제

때로는 기존 테스트와 유사하지만 약간의 변경 사항(예: 할당된 버전 변경)을 포함한 새로운 Test Run을 만드는 것이 매우 유용할 수 있습니다. ‘Create Test Run copy(테스트 실행 복사본 만들기)’ 메뉴 항목은 정확히 이러한 경우를 위해 만들어졌습니다. 이 메뉴 항목은 ‘Test Run’ 이슈 유형을 가진 이슈에만 사용할 수 있으며 다음 동작을 트리거합니다.

  • Test Run의 복사본과 여기에 할당된 Test Case Executions의 복사본을 만듭니다.
  • 새로 만들어진 모든 Test Case Execution 복사본은 ‘상위-하위’ 이슈 링크 유형을 사용하여 새 Test Run에 할당됩니다. 또한, 이러한 복사본은 추적성을 유지하기 위해 ‘Execution’ 이슈 링크 유형으로 원래 테스트 케이스에 할당됩니다.
  • 새로 생성된 이슈에 대해 모든 상태와 통계는 포함되지 않습니다.

위의 논리는 ‘Create Test Run copy(테스트 실행 복사본 만들기)’ 작업 규칙을 사용하여 구현됩니다.

코드 블록 확장하기

var workflow = require('@jetbrains/youtrack-scripting-api/workflow');
var entities = require('@jetbrains/youtrack-scripting-api/entities');
var utils = require('../calculate-tms-stats/utils');

exports.rule = entities.Issue.action({
  title: 'Create Test Run copy',
  command: 'Test Run Creation',
  guard: function(ctx) {
    return ctx.issue.isReported && (ctx.issue.Type.name == ctx.Type.TestRun.name);
  },
  action: function(ctx) {
    var issue = ctx.issue;
    var TestRunCopy = issue.copy(issue.project);
    TestRunCopy.Status = ctx.Status.InProgress; 
    var oldTestList = issue.links[ctx.Subtask.outward];
    oldTestList.forEach(function(v) {
      var newTest = v.copy(v.project);
      newTest.Status = ctx.Status.InProgress;
      newTest.links[ctx.Subtask.inward].delete(issue);
      newTest.links[ctx.Subtask.inward].add(TestRunCopy);
    });
    utils.resetStatuses(issue, TestRunCopy); 
    var newTestRunLink = ' ' + TestRunCopy.id + '';
    var message = 'New Test Run has been created ' + newTestRunLink + '.';
    workflow.message(message);
  },
  requirements: {
    Execution: {
      type: entities.IssueLinkPrototype,
      name: 'Execution',
      inward: 'Execution',
      outward: 'Assigned Test case or test suite'
    },
    Subtask: {
      type: entities.IssueLinkPrototype,
      name: 'Subtask',
      inward: 'subtask of',
      outward: 'parent for'
    },
    Type: {
      type: entities.EnumField.fieldType,
      TestRun: {
        name: "Test Run"
      },
    },
    Status: {
      type: entities.EnumField.fieldType,
      InProgress: {
        name: 'No Run'
      },
    }
  }
});
 

사용자의 동작 제한

테스트 시나리오로 작업하는 동안 잠재적인 실수(예: 잘못된 이슈 유형 사용)를 방지하기 위해 사용자의 가능한 동작을 제한할 수 있습니다. 예를 들어, 사용자가 Test Case를 Test Run에 연결하도록 하는 동작을 구현할 수 있습니다. 사용자가 Test Run을 이슈에 연결하면 이 동작은 연결된 이슈 유형이 ‘Test Case’인지 ‘Test Suite’인지 확인합니다. 둘 모두 아니면 이 동작은 사용자가 더 이상 진행하지 못하도록 경고를 보냅니다.

다음 코드 블록을 ‘populate test run’ 워크플로에 추가할 수 있습니다.

코드 블록 확장하기

 var message = ' ' + TestCase.id + '';
      workflow.check((TestCase.Type.name == ctx.Type.TestCase.name) || (TestCase.Type.name == ctx.Type.TestSuite.name), workflow.i18n('\'Test Run\' can be linked to \'Test Case\' and \'Test Suite\' only, but {0} has \'{1}\' type!', message, TestCase.Type.name));
    

위에서 언급한 바와 같이, Test Run 상태는 Test Executions의 진행 상황에 따라 달라지므로 읽기 전용이어야 합니다. 이는 ‘Test Run’ 이슈 유형을 가진 이슈에 대해 수동으로 상태 변경을 하는 것을 제한하는 코드 블록을 포함시켜 간단하게 처리할 수 있습니다.


예를 들어, ‘status-management(상태 관리)’ 워크플로에서 ‘Test Run’ 이슈 유형에 대한 ‘전환’ 섹션을 다음과 같이 조정할 수 있습니다.

코드 블록 확장하기

   'Failing': {
            targetState: 'Failing',
            action: function(ctx) {
              workflow.check(false, workflow.i18n('Test run has read-only status which is defined based on assigned tests statuses'));
            }
          },
          ...
        }

보고 및 추적 기능

보고

하루가 마무리되고 테스트 주기가 완료되면 결과를 분석해야 할 수 있습니다. 이를 위해 YouTrack 보고 기능을 사용할 수 있습니다. 테스트 관리 절차와 관련된 주요 정보를 표시하는 보고서 유형을 선택할 수 있습니다. 여기서는 데모 목적으로 대시보드에 2개의 보고서를 추가합니다.

  • ‘버전’ 필드로 필터링된 누적 흐름 보고서. 이러한 종류의 상태 보고서는 특정 제품 버전을 나타냅니다. 이 보고서는 타임라인에 매핑된 ‘Passed’, ‘Failed’ 및 ‘No Run’ 상태의 테스트 수와 같은 주요 메트릭을 보여줍니다. 교차 버전 데이터를 표시하려면 각 버전에 대한 보고서를 작성해야 합니다. 모든 데이터를 한 곳에서 작업할 수 있도록 모든 보고서를 대시보드에 추가하는 것이 편리할 수 ​​있습니다.

    보고서 설정:

    ApplyingCommandForSubsystem

    보고서 결과:

    ApplyingCommandForSubsystem

  • 이슈 분포 보고서. 이 보고서는 여러 제품 버전에 대한 주요 메트릭(예: 각 상태에 대한 Test Run 수)을 포함한 스냅샷입니다. 특히, 이 데모에는 버전을 서로 비교하고 버전 안정성을 분석하는 데 도움이 되는 보고서 결과가 포함되어 있습니다.

    보고서 설정:

    ApplyingCommandForSubsystem

    보고서 결과:

    ApplyingCommandForSubsystem

추적 기능

YouTrack은 ‘Test Execution’ 유형의 각 이슈에 대해 주요 테스트 주기 관련 정보를 저장하므로 언제든지 테스트 기록에 액세스하여 관련 버그를 확인할 수 있습니다.
Test Case가 있는 경우, 이 케이스가 관련된 모든 Test Executions를 참조할 수 있습니다.

Test Case Execution을 통해 Test Run을 참조할 수 있습니다.

실패한 Test Case Execution의 경우, 항상 관련 버그 목록을 참조할 수 있습니다.

이 문서에 설명된 워크플로에 대해 자세히 알아보거나 다른 워크플로 관련 주제를 논의하려면 Slack에서 당사 YouTrack 워크플로 커뮤니티에 참여하세요.

이 게시물은 Leonid Zalog가 작성한 Make It Workflow — Part 13: Supporting Test Management Scenarios를 번역한 글입니다.

구독

업데이트 구독