FlaUI实战:5分钟打造Win32遗留系统的自动化巡检方案

当那些服役超过十年的Win32客户端系统仍在生产环境中运行时,测试团队往往面临着一个尴尬的困境——既无法获取源代码进行单元测试,又不得不应对频繁的版本验证需求。某金融企业的案例颇具代表性:他们的交易客户端采用MFC框架开发,每周需要人工验证138个菜单项的功能完整性,耗时超过20人时。直到他们发现了FlaUI这个隐藏在.NET生态中的利器。

1. 环境准备与基础配置

在开始构建自动化巡检机器人之前,我们需要搭建合适的工作环境。不同于现代Web应用的测试框架,Win32自动化对运行环境有特殊要求:

  • 开发环境 :Visual Studio 2022社区版(免费)已足够,需确保安装了.NET 6+开发 workload
  • 目标机器 :必须启用UI Automation服务,可通过控制面板→轻松使用→轻松使用中心→"使鼠标更易于使用"→勾选"激活窗口悬停"
  • 权限要求 :测试执行账户需要与被测应用相同的权限级别,特别是对于银行、医疗等行业的客户端软件

安装FlaUI只需简单的NuGet命令:

Install-Package FlaUI.UIA3 -Version 3.2.0

提示:建议始终使用UIA3而非UIA2模式,前者基于微软较新的UI Automation API,对Win32控件支持更完善

基础验证代码可以这样写:

using FlaUI.Core;
using FlaUI.UIA3;

var app = Application.Launch("C:\\LegacyApp\\client.exe");
using (var automation = new UIA3Automation())
{
    var mainWindow = app.GetMainWindow(automation);
    Console.WriteLine($"成功连接至: {mainWindow.Title}");
}

2. Win32控件定位的实战技巧

老旧Win32应用的UI元素识别是最大挑战。不同于WPF的XAML结构,Win32控件通常只暴露最基本的属性。通过实践总结出以下定位策略:

控件属性优先级参考表

属性 稳定性 适用场景 获取方式
AutomationId ★★★★ 按钮/菜单项 Inspect工具查看
Name ★★★☆ 静态文本/对话框标题 NameProperty
ControlType ★★☆☆ 区分菜单/按钮等基础类型 ControlTypeProperty
Bounding ★☆☆☆ 绝对坐标定位(最后手段) BoundingRectangleProperty

实际定位时推荐组合使用FindAllDescendants和条件查询:

var saveButton = mainWindow.FindFirstDescendant(
    cf => cf.ByAutomationId("ID_SAVE")
            .And(cf.ByControlType(ControlType.Button)));

对于特别顽固的控件,可以尝试基于视觉树的递归搜索:

static AutomationElement FindDeepButton(AutomationElement parent, string name)
{
    var walker = TreeWalker.RawViewWalker;
    var element = walker.GetFirstChild(parent);
    while (element != null)
    {
        if (element.ControlType == ControlType.Button 
            && element.Name == name)
            return element;
        
        var result = FindDeepButton(element, name);
        if (result != null) 
            return result;
            
        element = walker.GetNextSibling(element);
    }
    return null;
}

3. 构建自动化巡检流程

一个完整的巡检流程通常包含四个阶段:初始化→登录→功能验证→结果收集。以下是证券交易客户端的典型实现:

初始化阶段注意事项

  • 使用 Application.Attach 连接已运行实例比重新启动更稳定
  • 设置合理的超时时间: automation.ConnectionTimeout = 3000
  • 对COM组件初始化失败的异常处理要完善

登录模块的健壮实现:

public void AutoLogin(string user, string pwd)
{
    var userField = mainWindow.FindFirstDescendant(
        cf => cf.ByControlType(ControlType.Edit).And(cf.ByAutomationId("ID_USER")));
    userField.Patterns.Value.Pattern.SetValue(user);
    
    // 密码框特殊处理
    var pwdField = mainWindow.FindFirstDescendant(
        cf => cf.ByControlType(ControlType.Edit).And(cf.ByName("密码")));
    pwdField.Click();
    Keyboard.Type(pwd);
    
    // 处理可能的安全警告
    TryDismissSecurityPopup();
}

菜单遍历的实用技巧:

var menuItems = mainWindow.FindAllDescendants(
    cf => cf.ByControlType(ControlType.MenuItem));
    
foreach (var item in menuItems)
{
    try 
    {
        item.Click();
        Logger.Info($"成功点击菜单: {item.Name}");
        
        // 验证内容区域更新
        if (VerifyContentUpdate())
            continue;
            
        throw new Exception("内容未按预期更新");
    }
    catch (Exception ex)
    {
        CaptureScreenshot($"MenuFail_{item.Name}");
        Logger.Error(ex);
    }
}

4. 异常处理与报告生成

在老旧系统自动化中,异常处理不是可选项而是必选项。建议构建三层防护体系:

  1. 元素级防护 :所有控件操作添加 TryXXX 包装

    public static bool TryClick(AutomationElement element)
    {
        try {
            element.Click();
            return true;
        }
        catch (ElementNotAvailableException) {
            return false;
        }
    }
    
  2. 流程级监控 :关键步骤添加超时检测

    public static bool WaitForElement(
        AutomationElement root, 
        Func<FindCondition, FindCondition> condition, 
        int timeoutMs = 3000)
    {
        var sw = Stopwatch.StartNew();
        while (sw.ElapsedMilliseconds < timeoutMs)
        {
            if (root.FindFirstDescendant(condition) != null)
                return true;
            Thread.Sleep(100);
        }
        return false;
    }
    
  3. 系统级恢复 :进程挂起检测与自动恢复

    if (app.HasExited)
    {
        Logger.Warn("应用异常退出,尝试重启");
        app = Application.Launch(appPath);
        automation = new UIA3Automation();
    }
    

报告生成建议采用HTML格式,包含:

  • 执行概览(通过率、耗时)
  • 失败步骤截图
  • 性能指标(各步骤响应时间)
  • 系统资源监控数据
public void GenerateReport(List<TestStep> steps)
{
    var html = new StringBuilder();
    html.AppendLine("<html><body>");
    html.AppendLine($"<h1>巡检报告 {DateTime.Now}</h1>");
    
    foreach (var step in steps)
    {
        html.AppendLine($"<div class='{(step.Success ? "pass" : "fail")}'>");
        html.AppendLine($"<h3>{step.Name}</h3>");
        if (!step.Success)
            html.AppendLine($"<img src='{step.ScreenshotPath}'/>");
        html.AppendLine("</div>");
    }
    
    File.WriteAllText("report.html", html.ToString());
}

5. 性能优化实战技巧

当巡检用例超过50个时,执行效率成为关键问题。通过以下几个方法可以将执行时间缩短40%以上:

元素缓存策略

// 首次查找后缓存常用元素
private AutomationElement _saveButton;
public AutomationElement SaveButton => 
    _saveButton ??= mainWindow.FindFirstDescendant(
        cf => cf.ByAutomationId("ID_SAVE"));

并行执行优化

var options = new ParallelOptions { MaxDegreeOfParallelism = 3 };
Parallel.ForEach(testCases, options, tc => 
{
    var localAutomation = new UIA3Automation();
    try {
        tc.Execute(localAutomation);
    }
    finally {
        localAutomation.Dispose();
    }
});

智能等待替代固定延迟

public static void WaitForBusyState(
    AutomationElement element, 
    int timeoutMs = 5000)
{
    var sw = Stopwatch.StartNew();
    while (sw.ElapsedMilliseconds < timeoutMs)
    {
        var pattern = element.Patterns.ItemContainer.PatternOrDefault;
        if (pattern == null || !pattern.IsBusy)
            return;
            
        Thread.Sleep(100);
    }
    throw new TimeoutException();
}

在某个保险核心系统的实践中,通过上述优化将原本需要85分钟的巡检缩短到了47分钟,同时稳定性从78%提升到了93%。

更多推荐