在软件开发生命周期中,测试阶段是确保产品质量的关键环节。然而,许多团队仍然依赖手动方式来创建测试用例执行排期表,这不仅耗时费力,还容易出错。本文将详细介绍如何通过自动化工具和脚本实现一键生成测试用例执行排期表,彻底告别手动排期的低效与混乱。

为什么需要自动化测试排期表

手动排期的痛点

手动创建测试排期表存在诸多问题。首先,测试用例数量庞大,动辄数百甚至上千个,手动整理和安排执行顺序极其耗时。其次,测试用例之间可能存在依赖关系,手动管理这些依赖关系容易出错。再者,当需求变更或测试用例更新时,手动调整排期表既繁琐又容易遗漏。

自动化排期的优势

自动化生成排期表可以显著提高效率。通过脚本自动分析测试用例的优先级、复杂度和依赖关系,可以在几秒钟内生成合理的执行计划。此外,自动化工具可以实时响应变化,当测试用例或资源发生变动时,能够快速重新生成排期表,确保计划的准确性和时效性。

实现方案概述

我们将使用Python结合一些开源库来实现自动化排期表生成。主要步骤包括:

  1. 读取测试用例数据(从Excel、CSV或数据库)
  2. 分析用例属性(优先级、预估时间、依赖关系)
  3. 应用排期算法(考虑资源限制和时间窗口)
  4. 生成排期表(Excel或HTML格式)

详细实现步骤

1. 数据准备与读取

首先,我们需要定义测试用例的数据结构。假设我们的测试用例信息存储在Excel文件中,包含以下列:用例ID、用例名称、优先级、预估执行时间(分钟)、依赖用例ID、执行人员。

import pandas as pd
from datetime import datetime, timedelta

# 读取测试用例数据
def load_test_cases(file_path):
    """
    从Excel文件读取测试用例数据
    参数:
        file_path: Excel文件路径
    返回:
        DataFrame格式的测试用例数据
    """
    df = pd.read_excel(file_path)
    # 数据清洗和预处理
    df['优先级'] = pd.Categorical(df['优先级'], categories=['P0', 'P1', 'P2', 'P3'], ordered=True)
    df['预估执行时间'] = df['预估执行时间'].fillna(30)  # 默认30分钟
    df['依赖用例ID'] = df['依赖用例ID'].fillna('无')
    return df

# 示例数据创建(用于演示)
def create_sample_data():
    """创建示例测试用例数据"""
    data = {
        '用例ID': ['TC001', 'TC002', 'TC003', 'TC004', 'TC005', 'TC006'],
        '用例名称': ['用户登录', '用户注册', '密码重置', '商品搜索', '加入购物车', '订单支付'],
        '优先级': ['P0', 'P1', 'P2', 'P1', 'P0', 'P0'],
        '预估执行时间': [15, 20, 25, 10, 30, 40],
        '依赖用例ID': ['无', '无', '无', '无', 'TC001', 'TC004'],
        '执行人员': ['张三', '李四', '王五', '张三', '李四', '王五']
    }
    return pd.DataFrame(data)

# 使用示例
# test_cases = load_test_cases('test_cases.xlsx')
test_cases = create_sample_data()
print("测试用例数据:")
print(test_cases)

2. 排期算法实现

排期算法需要考虑多个因素:优先级、依赖关系、执行人员时间窗口等。我们将实现一个基于优先级和依赖关系的智能排期算法。

class TestScheduler:
    def __init__(self, test_cases, start_time, daily_start_hour=9, daily_end_hour=18):
        """
        初始化测试排期器
        参数:
            test_cases: 测试用例DataFrame
            start_time: 排期开始时间
            daily_start_hour: 每日工作开始时间(24小时制)
            daily_end_hour: 每日工作结束时间(24小时制)
        """
        self.test_cases = test_cases.copy()
        self.start_time = datetime.strptime(start_time, "%Y-%m-%d %H:%M")
        self.daily_start_hour = daily_start_hour
        self.daily_end_hour = daily_end_hour
        self.schedule = []
        
    def check_dependencies(self, case_id, scheduled_ids):
        """检查用例依赖是否已满足"""
        if case_id == '无':
            return True
        dependencies = case_id.split(',')
        return all(dep in scheduled_ids for dep in dependencies)
    
    def get_next_available_slot(self, current_time, duration):
        """获取下一个可用的时间段"""
        # 如果当前时间在工作时间内
        current_hour = current_time.hour
        if self.daily_start_hour <= current_hour < self.daily_end_hour:
            # 检查当前时间段是否足够
            end_time = current_time + timedelta(minutes=duration)
            if end_time.hour < self.daily_end_hour:
                return current_time, end_time
        
        # 否则,安排到下一个工作日的开始时间
        next_day = current_time.date() + timedelta(days=1)
        next_start = datetime.combine(next_day, datetime.min.time().replace(hour=self.daily_start_hour))
        end_time = next_start + timedelta(minutes=duration)
        return next_start, end_time
    
    def generate_schedule(self):
        """生成排期表"""
        # 按优先级排序,优先级高的先排
        sorted_cases = self.test_cases.sort_values(by='优先级')
        
        scheduled_ids = set()
        current_time = self.start_time
        
        for _, case in sorted_cases.iterrows():
            # 检查依赖是否满足
            if not self.check_dependencies(case['依赖用例ID'], scheduled_ids):
                # 如果依赖未满足,将该用例移到最后
                continue
                
            # 获取可用时间段
            start_time, end_time = self.get_next_available_slot(current_time, case['预估执行时间'])
            
            # 记录排期
            self.schedule.append({
                '用例ID': case['用例ID'],
                '用例名称': case['用例名称'],
                '优先级': case['优先级'],
                '执行人员': case['执行人员'],
                '开始时间': start_time,
                '结束时间': end_time,
                '持续时间': f"{case['预估执行时间']}分钟"
            })
            
            scheduled_ids.add(case['用例ID'])
            current_time = end_time
        
        # 处理依赖未满足的用例(重新尝试)
        remaining_cases = self.test_cases[~self.test_cases['用例ID'].isin(scheduled_ids)]
        if not remaining_cases.empty:
            # 简单的重试机制,实际应用中可能需要更复杂的逻辑
            for _, case in remaining_cases.iterrows():
                start_time, end_time = self.get_next_available_slot(current_time, case['预估执行时间'])
                self.schedule.append({
                    '用例ID': case['用例ID'],
                    '用例名称': case['用例名称'],
                    '优先级': case['优先级'],
                    '执行人员': case['执行人员'],
                    '开始时间': start_time,
                    '结束时间': end_time,
                    '持续时间': f"{case['预估执行时间']}分钟"
                })
                current_time = end_time
        
        return pd.DataFrame(self.schedule)

# 使用示例
scheduler = TestScheduler(test_cases, "2024-01-15 09:00")
schedule_df = scheduler.generate_schedule()
print("\n生成的排期表:")
print(schedule_df)

3. 优化排期算法

上面的算法比较简单,实际应用中可能需要考虑更多因素,如人员冲突、并行执行等。下面是一个更高级的版本,支持多人员并行排期。

class AdvancedTestScheduler:
    def __init__(self, test_cases, start_time, daily_start_hour=9, daily_end_hour=18, max_parallel=3):
        self.test_cases = test_cases.copy()
        self.start_time = datetime.strptime(start_time, "%Y-%m-%d %H:%M")
        self.daily_start_hour = daily_start_hour
        self.daily_end_hour = daily_end_hour
        self.max_parallel = max_parallel  # 最大并行执行数
        self.schedule = []
        
    def generate_schedule(self):
        """生成支持并行执行的排期表"""
        # 按优先级和依赖关系排序
        sorted_cases = self.test_cases.sort_values(by=['优先级', '用例ID'])
        
        # 按执行人员分组
        assignees = sorted_cases['执行人员'].unique()
        
        # 为每个执行人员创建时间跟踪
        assignee_times = {assignee: self.start_time for assignee in assignees}
        
        scheduled_ids = set()
        
        for _, case in sorted_cases.iterrows():
            # 检查依赖
            if not self.check_dependencies(case['依赖用例ID'], scheduled_ids):
                continue
                
            assignee = case['执行人员']
            current_time = assignee_times[assignee]
            
            # 获取可用时间段
            start_time, end_time = self.get_next_available_slot(current_time, case['预估执行时间'])
            
            # 更新该人员的下次可用时间
            assignee_times[assignee] = end_time
            
            self.schedule.append({
                '用例ID': case['用例ID'],
                '用例名称': case['用例名称'],
                '优先级': case['优先级'],
                '执行人员': assignee,
                '开始时间': start_time,
                '结束时间': end_time,
                '持续时间': f"{case['预估执行时间']}分钟"
            })
            
            scheduled_ids.add(case['用例ID'])
        
        return pd.DataFrame(self.schedule)
    
    def check_dependencies(self, case_id, scheduled_ids):
        """检查依赖"""
        if case_id == '无':
            return True
        dependencies = case_id.split(',')
        return all(dep in scheduled_ids for dep in dependencies)
    
    def get_next_available_slot(self, current_time, duration):
        """获取可用时间段"""
        current_hour = current_time.hour
        if self.daily_start_hour <= current_hour < self.daily_end_hour:
            end_time = current_time + timedelta(minutes=duration)
            if end_time.hour < self.daily_end_hour:
                return current_time, end_time
        
        next_day = current_time.date() + timedelta(days=1)
        next_start = datetime.combine(next_day, datetime.min.time().replace(hour=self.daily_start_hour))
        end_time = next_start + timedelta(minutes=duration)
        return next_start, end_time

# 使用高级排期器
advanced_scheduler = AdvancedTestScheduler(test_cases, "2024-01-15 09:00")
advanced_schedule = advanced_scheduler.generate_schedule()
print("\n高级排期表(支持并行):")
print(advanced_schedule)

4. 生成Excel排期表

将排期结果导出为Excel文件,方便团队查看和共享。

def export_to_excel(schedule_df, output_path):
    """
    将排期表导出到Excel
    参数:
        schedule_df: 排期DataFrame
        output_path: 输出文件路径
    """
    # 格式化时间列
    schedule_df_export = schedule_df.copy()
    schedule_df_export['开始时间'] = schedule_df_export['开始时间'].dt.strftime('%Y-%m-%d %H:%M')
    schedule_df_export['结束时间'] = schedule_df_export['结束时间'].dt.strftime('%Y-%m-%d %H:%M')
    
    # 创建ExcelWriter
    with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
        schedule_df_export.to_excel(writer, sheet_name='测试排期表', index=False)
        
        # 获取workbook对象
        workbook = writer.book
        worksheet = writer.sheets['测试排期表']
        
        # 设置列宽
        for column in worksheet.columns:
            max_length = 0
            column_letter = column[0].column_letter
            for cell in column:
                try:
                    if len(str(cell.value)) > max_length:
                        max_length = len(str(cell.value))
                except:
                    pass
            adjusted_width = min(max_length + 2, 50)
            worksheet.column_dimensions[column_letter].width = adjusted_width
        
        # 添加标题行样式
        for cell in worksheet[1]:
            cell.font = cell.font.copy(bold=True)
            cell.fill = cell.fill.copy(fill_type='solid', fgColor='CCCCCC')
    
    print(f"\n排期表已导出到: {output_path}")

# 导出示例
export_to_excel(advanced_schedule, '测试排期表_20240115.xlsx')

5. 生成HTML排期表

除了Excel,我们还可以生成HTML格式的排期表,便于在网页上展示或发送邮件。

def export_to_html(schedule_df, output_path):
    """
    将排期表导出为HTML格式
    参数:
        schedule_df: 排期DataFrame
        output_path: 输出HTML文件路径
    """
    # 格式化时间
    schedule_html = schedule_df.copy()
    schedule_html['开始时间'] = schedule_html['开始时间'].dt.strftime('%Y-%m-%d %H:%M')
    schedule_html['结束时间'] = schedule_html['结束时间'].dt.strftime('%Y-%m-%d %H:%M')
    
    # 生成HTML
    html_content = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>测试用例执行排期表</title>
        <style>
            body {{ font-family: Arial, sans-serif; margin: 20px; }}
            h1 {{ color: #333; }}
            table {{ border-collapse: collapse; width: 100%; margin-top: 20px; }}
            th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
            th {{ background-color: #4CAF50; color: white; }}
            tr:nth-child(even) {{ background-color: #f2f2f2; }}
            .priority-p0 {{ background-color: #ffcccc; }}
            .priority-p1 {{ background-color: #ffebcc; }}
            .priority-p2 {{ background-color: #ffffcc; }}
            .priority-p3 {{ background-color: #ccffcc; }}
        </style>
    </head>
    <body>
        <h1>测试用例执行排期表</h1>
        <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
        <table>
            <thead>
                <tr>
                    <th>用例ID</th>
                    <th>用例名称</th>
                    <th>优先级</th>
                    <th>执行人员</th>
                    <th>开始时间</th>
                    <th>结束时间</th>
                    <th>持续时间</th>
                </tr>
            </thead>
            <tbody>
    """
    
    for _, row in schedule_html.iterrows():
        priority_class = f"priority-{row['优先级'].lower()}"
        html_content += f"""
                <tr class="{priority_class}">
                    <td>{row['用例ID']}</td>
                    <td>{row['用例名称']}</td>
                    <td>{row['优先级']}</td>
                    <td>{row['执行人员']}</td>
                    <td>{row['开始时间']}</td>
                    <td>{row['结束时间']}</td>
                    <td>{row['持续时间']}</td>
                </tr>
        """
    
    html_content += """
            </tbody>
        </table>
    </body>
    </html>
    """
    
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(html_content)
    
    print(f"HTML排期表已导出到: {output_path}")

# 导出HTML示例
export_to_html(advanced_schedule, '测试排期表_20240115.html')

6. 完整自动化脚本

将所有功能整合到一个完整的自动化脚本中,实现一键生成排期表。

import pandas as pd
from datetime import datetime, timedelta
import os

class AutomatedTestScheduler:
    """自动化测试排期表生成器"""
    
    def __init__(self, config):
        """
        初始化配置
        config: 字典,包含以下键:
            - input_file: 输入Excel文件路径
            - output_dir: 输出目录
            - start_time: 排期开始时间,格式: "YYYY-MM-DD HH:MM"
            - daily_start_hour: 每日开始时间(小时)
            - daily_end_hour: 每日结束时间(小时)
            - max_parallel: 最大并行数
        """
        self.config = config
        self.test_cases = None
        self.schedule = None
    
    def load_data(self):
        """加载测试用例数据"""
        input_file = self.config['input_file']
        if not os.path.exists(input_file):
            raise FileNotFoundError(f"输入文件不存在: {input_file}")
        
        self.test_cases = pd.read_excel(input_file)
        # 数据预处理
        self.test_cases['优先级'] = pd.Categorical(
            self.test_cases['优先级'], 
            categories=['P0', 'P1', 'P2', 'P3'], 
            ordered=True
        )
        self.test_cases['预估执行时间'] = self.test_cases['预估执行时间'].fillna(30)
        self.test_cases['依赖用例ID'] = self.test_cases['依赖用例ID'].fillna('无')
        print(f"成功加载 {len(self.test_cases)} 条测试用例")
    
    def generate_schedule(self):
        """生成排期表"""
        if self.test_cases is None:
            raise ValueError("请先调用 load_data() 加载数据")
        
        # 使用高级排期算法
        scheduler = AdvancedTestScheduler(
            self.test_cases,
            self.config['start_time'],
            self.config.get('daily_start_hour', 9),
            self.config.get('daily_end_hour', 18),
            self.config.get('max_parallel', 3)
        )
        
        self.schedule = scheduler.generate_schedule()
        print(f"成功生成 {len(self.schedule)} 条排期记录")
        return self.schedule
    
    def export_results(self):
        """导出结果"""
        if self.schedule is None:
            raise ValueError("请先调用 generate_schedule() 生成排期")
        
        # 创建输出目录
        output_dir = self.config['output_dir']
        os.makedirs(output_dir, exist_ok=True)
        
        # 生成文件名
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        excel_path = os.path.join(output_dir, f'排期表_{timestamp}.xlsx')
        html_path = os.path.join(output_dir, f'排期表_{timestamp}.html')
        
        # 导出Excel
        export_to_excel(self.schedule, excel_path)
        
        # 导出HTML
        export_to_html(self.schedule, html_path)
        
        print("\n导出完成!")
        print(f"Excel文件: {excel_path}")
        print(f"HTML文件: {html_path}")
        
        return {
            'excel_path': excel_path,
            'html_path': html_path
        }
    
    def run(self):
        """一键执行完整流程"""
        print("=" * 50)
        print("开始生成测试用例执行排期表")
        print("=" * 50)
        
        try:
            self.load_data()
            self.generate_schedule()
            result = self.export_results()
            print("\n✅ 排期表生成成功!")
            return result
        except Exception as e:
            print(f"\n❌ 生成失败: {str(e)}")
            raise

# 配置示例
config = {
    'input_file': 'test_cases.xlsx',  # 输入文件
    'output_dir': 'output',           # 输出目录
    'start_time': '2024-01-15 09:00', # 开始时间
    'daily_start_hour': 9,            # 每日开始时间
    'daily_end_hour': 18,             # 每日结束时间
    'max_parallel': 3                 # 最大并行数
}

# 如果没有输入文件,创建示例数据
if not os.path.exists('test_cases.xlsx'):
    sample_df = create_sample_data()
    sample_df.to_excel('test_cases.xlsx', index=False)
    print("已创建示例输入文件: test_cases.xlsx")

# 运行自动化排期
if __name__ == "__main__":
    scheduler = AutomatedTestScheduler(config)
    scheduler.run()

高级功能扩展

1. 支持资源约束

在实际项目中,测试环境和设备资源可能是有限的。我们可以通过添加资源约束来优化排期。

class ResourceConstrainedScheduler:
    def __init__(self, test_cases, resources, start_time, daily_start_hour=9, daily_end_hour=18):
        self.test_cases = test_cases
        self.resources = resources  # 资源列表,如['环境1', '环境2']
        self.start_time = datetime.strptime(start_time, "%Y-%m-%d %H:%M")
        self.daily_start_hour = daily_start_hour
        self.daily_end_hour = daily_end_hour
        self.resource_usage = {resource: [] for resource in resources}  # 记录资源使用时间段
    
    def is_resource_available(self, resource, start_time, end_time):
        """检查资源在指定时间段是否可用"""
        for usage in self.resource_usage[resource]:
            # 检查时间段是否重叠
            if not (end_time <= usage['start'] or start_time >= usage['end']):
                return False
        return True
    
    def assign_resource(self, case_id, start_time, end_time):
        """为用例分配资源"""
        for resource in self.resources:
            if self.is_resource_available(resource, start_time, end_time):
                # 记录资源使用
                self.resource_usage[resource].append({
                    'case_id': case_id,
                    'start': start_time,
                    'end': end_time
                })
                return resource
        return None
    
    def generate_schedule(self):
        """生成考虑资源约束的排期表"""
        sorted_cases = self.test_cases.sort_values(by=['优先级', '预估执行时间'])
        schedule = []
        current_time = self.start_time
        
        for _, case in sorted_cases.iterrows():
            # 尝试找到可用的时间段和资源
            attempts = 0
            max_attempts = 100
            
            while attempts < max_attempts:
                # 检查当前时间段是否可用
                start_time, end_time = self.get_next_available_slot(current_time, case['预估执行时间'])
                
                # 尝试分配资源
                resource = self.assign_resource(case['用例ID'], start_time, end_time)
                
                if resource:
                    # 成功分配
                    schedule.append({
                        '用例ID': case['用例ID'],
                        '用例名称': case['用例名称'],
                        '优先级': case['优先级'],
                        '资源': resource,
                        '开始时间': start_time,
                        '结束时间': end_time,
                        '持续时间': f"{case['预估执行时间']}分钟"
                    })
                    current_time = end_time
                    break
                
                # 如果资源不可用,尝试下一个时间段
                current_time = end_time
                attempts += 1
        
        return pd.DataFrame(schedule)
    
    def get_next_available_slot(self, current_time, duration):
        """获取可用时间段"""
        current_hour = current_time.hour
        if self.daily_start_hour <= current_hour < self.daily_end_hour:
            end_time = current_time + timedelta(minutes=duration)
            if end_time.hour < self.daily_end_hour:
                return current_time, end_time
        
        next_day = current_time.date() + timedelta(days=1)
        next_start = datetime.combine(next_day, datetime.min.time().replace(hour=self.daily_start_hour))
        end_time = next_start + timedelta(minutes=duration)
        return next_start, end_time

# 使用示例
resources = ['测试环境A', '测试环境B', '测试环境C']
resource_scheduler = ResourceConstrainedScheduler(test_cases, resources, "2024-01-15 09:00")
resource_schedule = resource_scheduler.generate_schedule()
print("\n考虑资源约束的排期表:")
print(resource_schedule)

2. 与Jira/TestRail等工具集成

可以将排期表自动同步到项目管理工具中。

import requests
import json

class JiraIntegration:
    """Jira集成类"""
    
    def __init__(self, server, username, api_token):
        self.server = server
        self.username = username
        self.api_token = api_token
        self.base_url = f"{server}/rest/api/2"
    
    def create_test_execution(self, schedule_df, project_key, test_plan_name):
        """在Jira中创建测试执行"""
        # 创建测试计划
        test_plan_data = {
            "fields": {
                "project": {"key": project_key},
                "summary": test_plan_name,
                "description": "自动生成的测试执行计划",
                "issuetype": {"name": "Test Plan"}
            }
        }
        
        response = requests.post(
            f"{self.base_url}/issue",
            json=test_plan_data,
            auth=(self.username, self.api_token),
            headers={"Content-Type": "application/json"}
        )
        
        if response.status_code == 201:
            test_plan_key = response.json()['key']
            print(f"创建测试计划: {test_plan_key}")
            
            # 为每个测试用例创建测试执行
            for _, row in schedule_df.iterrows():
                execution_data = {
                    "fields": {
                        "project": {"key": project_key},
                        "summary": f"{row['用例名称']} - {row['开始时间']}",
                        "description": f"执行人员: {row['执行人员']}\n预计时间: {row['持续时间']}",
                        "issuetype": {"name": "Test Execution"},
                        "parent": {"key": test_plan_key}
                    }
                }
                
                exec_response = requests.post(
                    f"{self.base_url}/issue",
                    json=execution_data,
                    auth=(self.username, self.api_token),
                    headers={"Content-Type": "application/json"}
                )
                
                if exec_response.status_code == 201:
                    print(f"  创建测试执行: {exec_response.json()['key']}")
        
        return test_plan_key

# 使用示例(需要配置真实的Jira信息)
# jira = JiraIntegration("https://your-jira.atlassian.net", "your-email", "your-api-token")
# jira.create_test_execution(schedule_df, "PROJ", "2024-01-15 测试执行计划")

实际应用建议

1. 输入数据规范

为了确保排期表生成的准确性,建议规范输入数据格式:

  • 用例ID:唯一标识符
  • 优先级:P0(最高)到P3(最低)
  • 预估时间:以分钟为单位,合理估算
  • 依赖关系:多个依赖用逗号分隔
  • 执行人员:明确指定

2. 定期更新机制

建议将排期表生成集成到CI/CD流程中,每次测试用例更新后自动重新生成排期表。

3. 人工审核

虽然自动化工具可以生成合理的排期,但仍建议测试负责人进行人工审核,确保排期符合实际项目需求。

总结

通过本文介绍的自动化方案,您可以:

  1. 一键生成:只需运行一个脚本即可生成完整的测试排期表
  2. 智能排期:自动考虑优先级、依赖关系和资源约束
  3. 多格式输出:支持Excel和HTML格式,便于分享和展示
  4. 高度可定制:根据项目需求调整排期策略

这套方案可以显著提高测试团队的工作效率,减少手动排期的时间和错误,让测试工程师专注于更有价值的测试设计和执行工作。

提示:本文提供的代码示例可以直接使用,也可以根据您的具体需求进行修改和扩展。建议先在小规模项目中试用,验证效果后再推广到整个团队。