Другое

Получить ID тестового набора и плана с ID теста в Azure DevOps

Узнайте, как получить test_suite_id, test_plan_id и test_case_id в одном DataFrame с помощью Python и Azure DevOps API, исправив NULL‑значения при извлечении данных.

Как можно получить идентификаторы Test Suite и Test Plan вместе с идентификатором Test Case в одном DataFrame, используя Python для интеграции с Azure DevOps API?
Я пишу скрипт на Python, который извлекает test_run_id, test_result_id, test_case_id, test_suite_id и test_plan_id из Azure DevOps. В настоящее время я получаю только NULL‑значения для полей test_suite_id и test_plan_id. Какой правильный подход, чтобы получить эти ID в том же DataFrame, что и ID тест‑кейса?

Понимание иерархии объектов тестирования в Azure DevOps

В Azure DevOps объекты тестирования образуют иерархическую структуру:

  • Test Plans содержат Test Suites
  • Test Suites содержат Test Cases
  • Test Runs выполняют Test Cases из конкретных Test Suites

При получении результатов тестов API обычно возвращает только ID тест‑кейсов, но не ID родительских планов и наборов. Чтобы собрать все ID в один DataFrame, необходимо явно запросить эти связи.

Полное решение на Python

Ниже приведено комплексное решение с использованием библиотеки azure-devops-python-api:

python
from azure.devops.connection import Connection
from azure.devops.v6_0.work_item_tracking.models import Wiql
from azure.devops.v6_0.test.models import TestCaseResult
from azure.devops.v6_0.test_plan.models import TestCase, TestCaseResult as TestCaseResultv7
import pandas as pd
from typing import List, Dict, Any

def get_all_test_ids_with_relationships(organization: str, project: str, 
                                      personal_access_token: str) -> pd.DataFrame:
    """
    Получить test_run_id, test_result_id, test_case_id, test_suite_id и test_plan_id
    в одном DataFrame с помощью Azure DevOps Python API.
    """
    
    # Создание подключения
    credentials = BasicAuthentication('', personal_access_token)
    connection = Connection(base_url=f"https://dev.azure.com/{organization}", creds=credentials)
    
    # Получение клиентов
    wit_client = connection.clients.get_work_item_tracking_client()
    test_client = connection.clients.get_test_client()
    test_plan_client = connection.clients.get_test_plan_client()
    
    # Шаг 1: Получить все тест‑планы
    test_plans = []
    tp_query = Wiql(query="""SELECT [System.Id] FROM workitems WHERE [System.WorkItemType] = 'Test Plan'""")
    for plan in wit_client.query_by_wiql(tp_query).work_items:
        test_plans.append({
            'test_plan_id': plan.id,
            'test_plan_url': plan.url
        })
    
    # Шаг 2: Для каждого плана получить его наборы и тест‑кейсы
    all_test_cases = []
    
    for plan in test_plans:
        plan_id = plan['test_plan_id']
        
        # Получить все наборы в плане
        suites = test_plan_client.get_suites(project=project, plan_id=plan_id)
        
        for suite in suites:
            suite_id = suite.id
            
            # Получить все тест‑кейсы в наборе
            test_cases = test_plan_client.get_test_cases(
                project=project,
                plan_id=plan_id,
                suite_id=suite_id
            )
            
            for tc in test_cases:
                all_test_cases.append({
                    'test_case_id': tc.id,
                    'test_case_name': tc.name,
                    'test_suite_id': suite_id,
                    'test_suite_name': suite.name,
                    'test_plan_id': plan_id,
                    'test_plan_name': plan.name if hasattr(plan, 'name') else f"Plan {plan_id}"
                })
    
    # Шаг 3: Получить тест‑рансы и результаты
    test_results_data = []
    
    for plan in test_plans:
        plan_id = plan['test_plan_id']
        
        # Получить тест‑рансы для этого плана
        test_runs = test_client.get_test_runs(project=project, plan_id=plan_id)
        
        for run in test_runs:
            run_id = run.id
            
            # Получить результаты тестов для этого ранса
            test_results = test_client.get_test_results(project=project, run_id=run_id)
            
            for result in test_results:
                test_results_data.append({
                    'test_run_id': run_id,
                    'test_result_id': result.id,
                    'test_case_id': result.test_case.id,
                    'outcome': result.outcome,
                    'duration_in_ms': result.duration_in_ms
                })
    
    # Шаг 4: Объединить все данные в один DataFrame
    df_test_cases = pd.DataFrame(all_test_cases)
    df_test_results = pd.DataFrame(test_results_data)
    
    # Соединить результаты тестов с тест‑кейсами, чтобы получить ID наборов и планов
    merged_df = pd.merge(
        df_test_results,
        df_test_cases,
        on='test_case_id',
        how='left'
    )
    
    return merged_df

Пошаговая реализация

Шаг 1: Установить необходимые библиотеки

bash
pip install azure-devops pandas requests

Шаг 2: Настроить аутентификацию

python
from azure.devops.connection import Connection
from azure.devops.credentials import BasicAuthentication

# Данные вашей организации и проекта Azure DevOps
organization = "your-organization"
project = "your-project"
personal_access_token = "your-pat-token"

# Создание подключения
credentials = BasicAuthentication('', personal_access_token)
connection = Connection(base_url=f"https://dev.azure.com/{organization}", creds=credentials)

Шаг 3: Получить иерархию тест‑планов, наборов и кейсов

python
def get_test_plan_suite_case_relationships(organization: str, project: str, 
                                         personal_access_token: str) -> pd.DataFrame:
    """
    Получить полную иерархию: Test Plan -> Test Suite -> Test Case
    """
    
    connection = Connection(base_url=f"https://dev.azure.com/{organization}", 
                          creds=BasicAuthentication('', personal_access_token))
    
    test_plan_client = connection.clients.get_test_plan_client()
    wit_client = connection.clients.get_work_item_tracking_client()
    
    # Получить все тест‑планы
    tp_query = Wiql(query="""SELECT [System.Id] FROM workitems WHERE [System.WorkItemType] = 'Test Plan'""")
    test_plans = wit_client.query_by_wiql(tp_query).work_items
    
    relationships = []
    
    for plan in test_plans:
        plan_id = plan.id
        plan_name = plan.fields.get('System.Title', f'Test Plan {plan_id}')
        
        # Получить все наборы в плане
        suites = test_plan_client.get_suites(project=project, plan_id=plan_id)
        
        for suite in suites:
            suite_id = suite.id
            suite_name = suite.name
            
            # Получить все тест‑кейсы в наборе
            test_cases = test_plan_client.get_test_cases(
                project=project,
                plan_id=plan_id,
                suite_id=suite_id
            )
            
            for tc in test_cases:
                relationships.append({
                    'test_plan_id': plan_id,
                    'test_plan_name': plan_name,
                    'test_suite_id': suite_id,
                    'test_suite_name': suite_name,
                    'test_case_id': tc.id,
                    'test_case_name': tc.name
                })
    
    return pd.DataFrame(relationships)

Шаг 4: Получить тест‑рансы и результаты

python
def get_test_runs_and_results(organization: str, project: str, 
                            personal_access_token: str) -> pd.DataFrame:
    """
    Получить тест‑рансы и их результаты
    """
    
    connection = Connection(base_url=f"https://dev.azure.com/{organization}", 
                          creds=BasicAuthentication('', personal_access_token))
    
    test_client = connection.clients.get_test_client()
    test_plan_client = connection.clients.get_test_plan_client()
    
    # Получить все тест‑планы
    tp_query = Wiql(query="""SELECT [System.Id] FROM workitems WHERE [System.WorkItemType] = 'Test Plan'""")
    test_plans = wit_client.query_by_wiql(tp_query).work_items
    
    all_results = []
    
    for plan in test_plans:
        plan_id = plan.id
        
        # Получить тест‑рансы для этого плана
        test_runs = test_client.get_test_runs(project=project, plan_id=plan_id)
        
        for run in test_runs:
            run_id = run.id
            
            # Получить результаты тестов для этого ранса
            test_results = test_client.get_test_results(project=project, run_id=run_id)
            
            for result in test_results:
                all_results.append({
                    'test_run_id': run_id,
                    'test_result_id': result.id,
                    'test_case_id': result.test_case.id,
                    'outcome': result.outcome,
                    'duration_in_ms': result.duration_in_ms,
                    'test_plan_id': plan_id  # Мы знаем это из внешнего цикла
                })
    
    return pd.DataFrame(all_results)

Шаг 5: Объединить всё в один DataFrame

python
def get_complete_test_dataframe(organization: str, project: str, 
                              personal_access_token: str) -> pd.DataFrame:
    """
    Получить полный DataFrame со всеми ID тестов
    """
    
    # Получить иерархию связей
    relationships_df = get_test_plan_suite_case_relationships(organization, project, personal_access_token)
    
    # Получить тест‑рансы и результаты
    results_df = get_test_runs_and_results(organization, project, personal_access_token)
    
    # Объединить всё
    complete_df = pd.merge(
        results_df,
        relationships_df,
        on=['test_case_id', 'test_plan_id'],
        how='left'
    )
    
    return complete_df

Ключевые конечные точки API

Согласно документации Microsoft, вот ключевые конечные точки API, которые вы будете использовать:

  1. Получить Test Plans: GET https://dev.azure.com/{organization}/{project}/_apis/test/Plans?api-version=7.1

  2. Получить Test Suites по плану: GET https://dev.azure.com/{organization}/{project}/_apis/test/Plans/{planId}/Suites?api-version=7.1

  3. Получить Test Cases по набору: GET https://dev.azure.com/{organization}/{project}/_apis/testplan/Plans/{planId}/Suites/{suiteId}/TestCase?api-version=7.1

  4. Получить Test Runs: GET https://dev.azure.com/{organization}/{project}/_apis/test/Runs?api-version=7.1

  5. Получить Test Results: GET https://dev.azure.com/{organization}/{project}/_apis/test/Runs/{runId}/Results?api-version=7.1

Частые ошибки и решения

Ошибка 1: Получение NULL‑значений для ID наборов и планов

Проблема: При прямом получении результатов тестов они не включают информацию о наборе и плане.

Решение: Используйте иерархический подход – сначала получите все связи, затем объедините их с результатами тестов.

Ошибка 2: Проблемы с производительностью в больших проектах

Проблема: Слишком много запросов к API может замедлить выполнение.

Решение: Используйте пакетные операции и пагинацию, где это возможно:

python
# Пример: Получить тест‑кейсы с пагинацией
def get_all_test_cases_with_pagination(organization, project, plan_id, suite_id):
    test_cases = []
    skip = 0
    top = 1000  # Максимум элементов на странице
    
    while True:
        response = test_plan_client.get_test_cases(
            project=project,
            plan_id=plan_id,
            suite_id=suite_id,
            top=top,
            skip=skip
        )
        
        test_cases.extend(response.value)
        
        if len(response.value) < top:
            break
            
        skip += top
    
    return test_cases

Ошибка 3: Проблемы с аутентификацией

Проблема: Ошибки аутентификации.

Решение: Убедитесь, что ваш PAT имеет правильные разрешения:

python
# Необходимые разрешения для Personal Access Token:
# - Test Plans: Read
# - Test Runs: Read
# - Work Items: Read

Ошибка 4: Отсутствие данных в объединениях

Проблема: Некоторые тест‑кейсы могут не иметь рансов, или некоторые рансы могут ссылаться на тест‑кейсы, которые не находятся в текущем запросе.

Решение: Используйте подходящие типы объединений и обрабатывайте отсутствующие данные:

python
# Левое объединение, чтобы сохранить все результаты тестов, даже если связи не найдены
complete_df = pd.merge(results_df, relationships_df, on='test_case_id', how='left')

# Или внутреннее объединение, чтобы получить только результаты с полной информацией
complete_df = pd.merge(results_df, relationships_df, on='test_case_id', how='inner')

Альтернативный подход с использованием Test Plan API

Для повышения производительности, особенно в больших проектах, можно использовать напрямую Test Plan API:

python
def get_complete_data_using_test_plan_api(organization: str, project: str, 
                                        personal_access_token: str) -> pd.DataFrame:
    """
    Альтернативный подход, использующий Test Plan API напрямую для лучшей производительности
    """
    
    connection = Connection(base_url=f"https://dev.azure.com/{organization}", 
                          creds=BasicAuthentication('', personal_access_token))
    
    test_plan_client = connection.clients.get_test_plan_client()
    test_client = connection.clients.get_test_client()
    wit_client = connection.clients.get_work_item_tracking_client()
    
    # Шаг 1: Получить все тест‑планы
    tp_query = Wiql(query="""SELECT [System.Id] FROM workitems WHERE [System.WorkItemType] = 'Test Plan'""")
    test_plans = wit_client.query_by_wiql(tp_query).work_items
    
    all_data = []
    
    for plan in test_plans:
        plan_id = plan.id
        plan_name = plan.fields.get('System.Title', f'Test Plan {plan_id}')
        
        # Шаг 2: Получить все наборы в плане
        suites = test_plan_client.get_suites(project=project, plan_id=plan_id)
        
        for suite in suites:
            suite_id = suite.id
            suite_name = suite.name
            
            # Шаг 3: Получить все тест‑кейсы в наборе
            test_cases = test_plan_client.get_test_cases(
                project=project,
                plan_id=plan_id,
                suite_id=suite_id
            )
            
            for tc in test_cases:
                # Шаг 4: Найти тест‑рансы для этого тест‑кейса
                test_runs = test_client.get_test_runs(
                    project=project,
                    plan_id=plan_id,
                    test_case_id=tc.id
                )
                
                for run in test_runs:
                    # Шаг 5: Получить результаты тестов для этого ранса
                    test_results = test_client.get_test_results(project=project, run_id=run.id)
                    
                    for result in test_results:
                        all_data.append({
                            'test_plan_id': plan_id,
                            'test_plan_name': plan_name,
                            'test_suite_id': suite_id,
                            'test_suite_name': suite_name,
                            'test_case_id': tc.id,
                            'test_case_name': tc.name,
                            'test_run_id': run.id,
                            'test_result_id': result.id,
                            'outcome': result.outcome,
                            'duration_in_ms': result.duration_in_ms
                        })
    
    return pd.DataFrame(all_data)

Этот всесторонний подход гарантирует, что вы получите все необходимые ID (test_plan_id, test_suite_id, test_case_id, test_run_id, test_result_id) в одном DataFrame, устраняя проблему NULL‑значений.

Источники

  1. How to get Test Suite ID and Test Plan ID in the same dataframe as the Test Case ID? - Stack Overflow
  2. How to retrieve Test Results in Azure DevOps with Python REST API - Stack Overflow
  3. Suite Test Case - Get Test Case List - REST API (Azure DevOps Test Plan) | Microsoft Learn
  4. Test Suites - Get Test Cases - REST API (Azure DevOps Test) | Microsoft Learn
  5. azure-devops-python-api/azure-devops/azure/devops/released/test/test_client.py at dev · microsoft/azure-devops-python-api

Вывод

  • Используйте иерархический подход: сначала получите связи Test Plan → Test Suite → Test Case, затем объедините их с тест‑рансами и результатами.
  • Библиотека azure-devops-python-api предоставляет необходимые клиенты для доступа к API Azure DevOps.
  • Ключ к успеху – правильный порядок вызовов API, чтобы захватить все связи.
  • Обрабатывайте пагинацию и проблемы производительности в больших проектах.
  • Выбирайте подходящий тип объединения (left или inner) в зависимости от того, нужны ли вам все результаты или только полностью заполненные данные.
  • Убедитесь, что ваш Personal Access Token имеет все необходимые разрешения для чтения тест‑планов, рансов и work‑items.

Такой подход даст вам полный DataFrame со всеми ID тестов, которые вам нужны, устраняя проблему NULL‑значений для ID наборов и планов.

Авторы
Проверено модерацией
Модерация