Публикации и ответы на комментарии в блогах JetBrains не выходят на русском языке с 2022 года.

Приносим извинения за неудобства.

Best Practices Features YouTrack

Как построить процесс: сценарии управления тестами

Read this post in other languages:

Рады приветствовать вас в нашей серии статей «Как построить процесс» (остальные статьи серии доступны на английском языке)! Нас не раз просили рассказать о возможностях YouTrack для QA-команд. Сегодня мы рассмотрим работу со сценариями управления тестами (test management scenarios, TMS) и покажем, как настроить управление тестовой инфраструктурой из YouTrack без привлечения сторонних решений. Это не только позволит вам сэкономить ресурсы, но и значительно упростит процесс управления тестами.

Статья предназначена прежде всего для менеджеров по тестированию, но может пригодиться всем, кто интересуется рабочими процессами YouTrack. Мы покажем, как выстроить процесс тестирования при помощи YouTrack, дадим рекомендации по настройке проектов и задач, приведем примеры реализаций TMS и расскажем о многом другом.

Также мы подготовили набор готовых блоков кода, которые вы сможете использовать для автоматизации рабочих процессов управления тестами. Они позволят оптимизировать процедуру ассоциации тест-кейсов с тест-ранами, упростить клонирование тест-ранов, настроить подсказки системы, указывающие, какой тест исполнить следующим и т. д.

Настройки проекта управления тестами в YouTrack

Для проектов управления тестами в YouTrack используются специальные типы задач: тест-кейс, тестовый набор (совокупность тестов), тест-ран (совокупность тест-кейсов или тестовых наборов, ассоциированных с определенным циклом тестирования) и запуск тест-кейса (возникает для каждого тест-кейса в результате выполнения тест-рана). Вы можете настроить необходимые типы задач на уровне TMS-проекта. Существует 3 типа ссылок для обозначения отношений между задачами:

  • Стандартная ссылка «родитель-подзадача» обозначает иерархические отношения и используется для связи тест-ранов с запусками тест-кейсов, а также тест-кейсов с тестовыми наборами.
  • Настраиваемая ссылка «Execution» используется для связи тест-кейсов с их запусками.
  • Настраиваемая ссылка «Related Bug» используется для связи упавших тестов с соответствующими ошибками.

Описания тестовых требований можно хранить в виде статей, размещая ссылки на них в соответствующих задачах (в текстовом поле).
Вы можете дополнить структуру задач дополнительными настраиваемыми полями, такими как режим тестирования, категория, тестовый поток, применение, и указать для них допустимые значения (например, возможные статусы теста). Также вы можете использовать зависимые поля, видимость которых будет определяться типом задачи.

Вы можете корректировать набор предопределенных полей для задач типа «Test Case» и «Test Run», адаптируя их к потребностям вашего проекта.

Настройка тест-ранов и тестовых запусков

Предположим, вы хотите протестировать некоторую версию продукта. Для этого вам необходимо создать тест-ран и включить в него определенные тест-кейсы и тестовые наборы. Выполните следующие шаги:

  • Создайте задачу типа «Test Run».
  • Создайте для нее настраиваемую ссылку типа «Assigned test case and test suite».
  • Во всплывающем окне выберите тест-кейсы, которые вы хотите включить в тест-ран.

Дальнейшие шаги могут быть автоматизированы рабочим процессом «Populate Test Run»:

  • Для выбранных тест-кейсов и тестовых наборов будут созданы новые задачи типа «Test Case Execution».
  • С помощью ссылки типа «Execution» каждая из них будет соотнесена с исходным тест-кейсом.
  • В качестве родителя для новых задач будет указан созданный вами тест-ран.

Процедура выглядит следующим образом:

Развернуть блок кода

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 Case Execution» и «Test Run» выставлен статус «No Run».

Переключение между тестами

В ходе тестирования вы можете прерывать исполнение одного теста и переключаться на другой. Для этого есть два способа:

  1. Вручную изменить статус исполняемого теста (в списке задач).
  2. Открыть исполняемый тест и перейти к следующему тесту со статусом «No Run». Во всплывающем окне вам будет предложено, какой тест исполнить следующим.
    Также при помощи автоматизации вы можете:

    • Добавить проверку тест-рана на наличие тестов со статусом «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 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». В соответствии с изначальными настройками YouTrack статус тест-рана будет заполнен автоматически. Он предусматривает 4 возможных значения:

  • Failing: не все тесты были исполнены (хотя бы один имеет статус «No Run»), и есть хотя бы один тест со статусом «Failed».
  • Passing: не все тесты были исполнены (хотя бы один имеет статус «No Run»), и нет тестов со статусом «Failed».
  • Failed: все тесты были исполнены (нет тестов со статусом «No Run») и среди них есть хотя бы один тест со статусом «Failed».
  • Passed: нет ни одного теста со статусом «No Run» или «Failed».

Данный набор статусов позволит QA-команде следить за исполнением тестов на протяжении всего цикла тестирования. Важно отметить, что статус тест-рана зависит исключительно от результатов выполнения входящих в него тестов, и не должен меняться вручную. Он определяется с помощью рабочего процесса конечных автоматов для каждого типа задачи. Процесс также выполняет переключение тестов.

Развернуть блок кода


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'
    },
  }
});
 

Статистика

Скорее всего, вам будет интересно взглянуть на статистику выполненного цикла тестов. В рамках данного демо мы используем три метрики:

  • Суммарное количество тестов, включенных в данный тест-ран.
  • Количество тестов со статусом «Passed».
  • Количество тестов со статусом «Failed».


Метрики обновляются при каждом изменении статуса теста, а также при появлении в тест-ране нового теста и удалении старого. Код обновления метрик приведен ниже.

Развернуть блок кода

  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» и некоторые другие. Для того, чтобы реализовать свой набор метрик, вы можете добавить необходимые настраиваемые поля и дополнить логику рабочих процессов.

Отслеживание ошибок

Вы можете фиксировать ошибки, возникающие при падении тестов, создавая для каждой из них отдельную задачу. Для связи упавшего теста с ошибкой можно либо использовать настраиваемые типы ссылок, либо разместить ссылку на ошибку в настраиваемом текстовом поле. Вы можете завести отдельный проект YouTrack для отслеживания ошибок.

Возможные улучшения

Вы можете расширять функциональность YouTrack и адаптировать ее к новым бизнес-требованиям. Предлагаем несколько вариантов оптимизаций.

Клонирование тест-рана

Было бы удобно иметь возможность создавать новый тест-ран на основе существующего, внося лишь некоторые изменения (например, указывая другую версию продукта). Чтобы сделать это, воспользуйтесь командой меню «Create Test Run copy», доступной для всех задач типа «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 Suite». Действие будет запускаться каждый раз, когда пользователь связывает тест-ран с некоторой задачей. Если условие не выполняется, то YouTrack сообщит пользователю, что связать тест-ран с данной задачей невозможно.

Чтобы реализовать это действие, добавьте следующий блок кода в рабочий процесс «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».


Для этого в рабочем процессе «Status-management» отредактируйте секцию «transitions» для задач типа «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 предусмотрены отчеты. Вы можете выбрать тип отчета, отображающий наиболее актуальную информацию. В нашем примере на панель мониторинга будут добавлены два отчета:

  • Отчет «Совокупный поток» для указанной версии продукта. Такой тип отчета отфильтрует данные по значению поля «версия» и таким образом предоставит вам результаты тестирования для определенной версии вашего продукта. Он демонстрирует ключевые метрики (например, количество тестов со статусами «Passed», «Failed» и «No Run») с привязкой к хронологии. Если вас интересуют результаты для различных версий, необходимо сгенерировать такой отчет для каждой из них по отдельности. Для удобства вы можете добавить все отчеты на панель мониторинга — это позволит вам собрать все данные в одном месте и всегда иметь их под рукой.

    Настройка отчета:

    Применение команды для подсистемы

    Результаты:

    Применение команды для подсистемы

  • Отчет о распределении задач. Данный отчет демонстрирует ключевые метрики (например, количество тестов с тем или иным статусом) для нескольких версий продукта. В нашем примере такой отчет позволяет сравнить версии продукта с точки зрения их стабильности.

    Настройка отчета:

    Применение команды для подсистемы

    Результаты:

    Применение команды для подсистемы

Навигация

YouTrack автоматически сохраняет информацию об исполнении теста для всех задач типа «Test Execution».
Благодаря этому вам всегда доступна история тест-кейса. Вы можете просмотреть информацию о любом из его запусков и отследить имеющиеся ошибки.

Точно так же вы можете перейти от конкретного запуска теста к соответствующему тест-кейсу.

При неудачном исполнении тест-кейса вы всегда можете обратиться к списку связанных ошибок.

Вы можете загрузить источники для всех рабочих процессов, описанных в этой статье, из нашего репозитория. А чтобы узнать больше о рабочих процессах YouTrack и пообщаться на любые связанные с ними темы, присоединяйтесь к сообществу YouTrack Workflow в Slack.

Ваша команда YouTrack
The Drive to Develop

Discover more