ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Windows Terminal 测试实战:为 C++/WinRT XAML Islands 应用编写 TAEF 单元测试

Windows Terminal 测试实战:为 C++/WinRT XAML Islands 应用编写 TAEF 单元测试 Windows Terminal 测试实战为 C/WinRT XAML Islands 应用编写 TAEF 单元测试【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal当你用 C/WinRT 与 XAML Islands 构建了一个 Win32 桌面应用后如何为它编写单元测试这篇文章基于 Windows TerminalOpenConsole 仓库中的实践文档 Unittesting-CppWinRT-Xaml.md完整讲解如何为三类目标——纯 C 类、C/WinRT 组件、以及使用 XAML UI 的控件——搭建可运行的 TAEFTest Application Engineering Framework单元测试。读完本篇你将掌握「DLL 拆分为静态库 SxS 清单 AppxManifest 托管测试包」这套完整方案的每一步配置并能在当前仓库中对照真实的工程文件TerminalApp.UnitTests.vcxproj、TerminalAppLib.vcxproj逐一验证。背景为什么 C/WinRT 桌面应用难以测试Windows Terminal 的 UI 层由一组 C/WinRT 组件构成TerminalApp、TerminalControl、TerminalSettings等应用主体是 Win32 可执行文件 XAML Islands。这类应用的测试难点在于纯 C 代码藏在 DLL 里新建的 C/WinRT 组件默认输出为 DLL用于激活 WinRT 类型但 DLL 中那些并非 WinRT 类型的普通 C 类很难被另一个 TAEF 测试 DLL 直接链接调用WinRT 类型激活在 1903 之前的 Windows 上只有打包packaged代码才能激活 WinRT 类型。1903 引入了「非打包激活」但需要 SxS 清单配合XAML 类型激活XAML Hosting API 在 18295 版之后有较严格的要求测试宿主必须携带特定清单声明且 TAEF 的te.exe本身并未如此声明只能让 TAEF 以「临时打包环境」的方式运行测试。原文档记录了作者 Mike Griese 为 Windows Terminal 打通这三类测试的全过程。下面按原文档脉络展开并用当前仓库的实际工程文件佐证。前置条件版本与 CI 要求文档列出的三个前提条件仍然值得逐条核对C/WinRT NuGet 包版本至少使用2.0.190605.7。更早的版本存在静态库依赖检测的 bug——Visual Studio 中可能碰巧能编译但直接用 MSBuild 编译会失败。当前仓库实际使用的版本见 common.nugetversions.props第 7 行Microsoft.Windows.CppWinRT.2.0.250303.1已远超文档的最低要求。CI 系统版本如果要在 CI 上跑测试CI 机器至少需要 Windows 183621903。本地开发不受此限制仅 CI 需要。TAEF 适配器文档撰写时官方 TAEF VsTest 适配器存在无法在 UAP 上下文中运行测试的 bug团队使用了 TAEF 团队私下提供的补丁版本10.38.190610001-uapadmin。当前仓库已在 common.nugetversions.props第 11 行中固定为Microsoft.Taef.10.100.251104001说明该修复后来已随正式版本发布。第一步把 C/WinRT 实现搬进静态库这是整个方案的地基。目标结构是静态库负责编译所有代码.cpp/.h/.idl/.xamlDLL 项目退化为静态库的一层薄封装负责生成 .winmd 和打包。这样既能把原 DLL 继续作为产品发布又能把同一份实现链接进测试 DLL。以 Windows Terminal 为例原TerminalAppDLL 项目被拆成了TerminalAppLib.vcxproj静态库ConfigurationType为StaticLibrary见该文件第 9 行dll/TerminalApp.vcxproj保留DynamicLibrary配置第 9 行仅链接TerminalAppLib并生成最终的TerminalApp.dll与TerminalApp.winmd。创建静态库项目操作要点与原文档一致复制原 DLL 的.vcxproj到新文件更换ProjectGuid加入.sln把ConfigurationType改为StaticLibrary该库项目应负责构建全部头文件、.cpp、WinRT 类型的.idl以及.xaml文件——在 TerminalAppLib.vcxproj 中可以看到所有ApplicationDefinition如App.xaml与Page如TabRowControl.xaml、CommandPalette.xaml都声明在此文件中。两个原文档特别提醒的坑在当前仓库中依然可见目录隔离C/WinRT 把项目目录当作中间构建树的根每个目录只应放一个.vcxproj。因此TerminalApp项目实际放在了子目录dll/下见 dll/TerminalApp.vcxproj而静态库放在上一层。仓库中另一个可选方案是把源码集中在一个目录、由dll/与lib/两个子目录分别构建二进制。预编译头必须与项目同目录C/WinRT 不允许pch.h位于其他目录。所以静态库有自己的pch.h/pch.cppDLL 项目则放一个空的pch.h可对照 dll/TerminalApp.vcxproj 第 33、44 行只引用pch.h和pch.cpp。手动引用其他项目的 .winmd从 C/WinRT 静态库对其他 C/WinRT 项目使用ProjectReference会遇到难以解释的问题因此需要手动引用对方构建产物中的.winmd。TerminalAppLib.vcxproj第 435–484 行的做法是典型范式Reference IncludeMicrosoft.Terminal.Settings.Model HintPath$(OpenConsoleCommonOutDir)Microsoft.Terminal.Settings.Model\Microsoft.Terminal.Settings.Model.winmd/HintPath IsWinMDFiletrue/IsWinMDFile Privatefalse/Private CopyLocalSatelliteAssembliesfalse/CopyLocalSatelliteAssemblies /ReferencePrivatefalse与CopyLocalSatelliteAssembliesfalse的作用是阻止依赖向上游传播——否则会在使用方出现重复类型定义。HintPath需按自己的工程布局本地验证仓库统一使用$(OpenConsoleCommonOutDir)这一公共输出目录变量。更新 DLL 项目DLL 项目中删掉全部源码条目只留pch.h/pch.cpp以及 WinRT 类型的头文件然后链接静态库。文档中给的手动链接方式AdditionalLibraryDirectoriesAdditionalDependencies在 VS2017 时代是必要的当前仓库的 dll/TerminalApp.vcxproj第 77–83 行已经改用ProjectReference配合Privatetrue、CopyLocalSatelliteAssembliestrue注释里保留了历史说明!-- Reference TerminalAppLib here, so we can use its TerminalApp.winmd as our TerminalApp.winmd. This didnt work correctly in VS2017, youd need to manually reference the lib -- ProjectReference Include$(OpenConsoleDir)src\cascadia\TerminalApp\TerminalAppLib.vcxproj Privatetrue/Private CopyLocalSatelliteAssembliestrue/CopyLocalSatelliteAssemblies /ProjectReference文档强调的一点在当前实现中仍然成立不要把静态库项目的 .winmd 作为引用加入 DLL 项目自 2.0.190605.7 起的 CppWinRT 包已能自动判定静态库的.winmd应包含进最终包。另外两个文档提到的坑mdmerge 重复类型错误当依赖呈菱形C.dll依赖A.dll和B.dll而B.dll也依赖A.dll时会出现。解法是在中间依赖上给ProjectReference加Privatefalse、CopyLocalSatelliteAssembliesfalse。聚合资源的项目如果有.exe/打包项目聚合了所有.xbf/.pri拆分后要更新其指向新静态库。DLL 项目源码区的注释「DONT PUT XAML FILES HERE! Put them in TerminalAppLib.vcxproj」dll/TerminalApp.vcxproj 第 26 行就是对团队的日常提醒。第二步创建 TAEF 测试工程仓库中的测试工程位于 src/cascadia/ut_app/包含 TerminalApp.UnitTests.vcxprojConfigurationType为DynamicLibrary即 TAEF 测试 DLL、precomp.h 以及测试源码JsonUtilsTests.cpp、FzfTests.cpp。引用静态库最简单的一步给测试项目加一个指向静态库的ProjectReference实现代码即被链接进测试 DLLProjectReference Include$(OpenConsoleDir)\src\cascadia\TerminalApp\TerminalAppLib.vcxproj /当前 TerminalApp.UnitTests.vcxproj第 31–37 行就是这样引用TerminalAppLib、Microsoft.Terminal.Settings.ModelLib、types、propslib的。此后纯 C 类型可直接实例化——例如 JsonUtilsTests.cpp 直接测试JsonUtils的模板转换逻辑不需要任何 WinRT 激活。使用 C/WinRT 类型SxS 清单 ActivationContext要在非打包的测试 DLL 中激活自己编写的 WinRT 类型依赖的是 Windows 1903 引入的「非打包 WinRT 激活」。需要三件事1. 编写清单文件列出每个被依赖的 DLL 及其包含的可激活类型?xml version1.0 encodingutf-8 standaloneyes? assembly xmlnsurn:schemas-microsoft-com:asm.v1 manifestVersion1.0 file nameTerminalSettings.dll hashalgSHA1 activatableClass nameMicrosoft.Terminal.Settings.KeyChord threadingModelboth xmlnsurn:schemas-microsoft-com:winrt.v1/activatableClass activatableClass nameMicrosoft.Terminal.Settings.TerminalSettings threadingModelboth xmlnsurn:schemas-microsoft-com:winrt.v1/activatableClass /file file nameTerminalApp.dll hashalgSHA1 activatableClass nameTerminalApp.App threadingModelboth xmlnsurn:schemas-microsoft-com:winrt.v1/activatableClass activatableClass nameTerminalApp.AppKeyBindings threadingModelboth xmlnsurn:schemas-microsoft-com:winrt.v1/activatableClass activatableClass nameTerminalApp.XamlmetaDataProvider threadingModelboth xmlnsurn:schemas-microsoft-com:winrt.v1/activatableClass /file /assembly2. 把清单嵌入测试 DLL并在构建后复制到测试二进制旁边。嵌入用vcxproj属性完成TerminalApp.UnitTests.vcxproj 第 61–67 行与文档完全一致PropertyGroup GenerateManifesttrue/GenerateManifest EmbedManifesttrue/EmbedManifest /PropertyGroup ItemGroup Manifest IncludeTerminalApp.Unit.Tests.manifest / /ItemGroup复制到$(OutDir)则通过PostBuildEvent完成。文档给出的方式是用xcopy拷贝清单、以及测试依赖的各个 C/WinRT DLLTerminalConnection.dll、TerminalSettings.dll、TerminalControl.dll到输出目录因为激活要求实现 DLL 与测试二进制同目录。当前工程把这件事拆成了更清晰的两段TerminalApp.UnitTests.vcxproj 第 74–101 行PreBuildEvent把解决方案统一的 WindowsTerminal.manifest 拷贝到ut_app\TerminalApp.Unit.Tests.manifest保证全仓库只维护一份清单源文件PostBuildEvent把 SxS 清单和 AppxManifest 一并xcopy到$(OutDir)。值得注意的一个演进细节当前 TerminalApp.Unit.Tests.manifest 已不再逐类罗列activatableClass而是复用主应用的兼容性清单。其中有一段关键注释第 8–16 行compatibility xmlnsurn:schemas-microsoft-com:compatibility.v1 application !-- Windows 10 1903 -- !-- maxversiontested is CASE SENSITIVE. Do not change this.-- !-- DO NOT ADVANCE PAST 18362. The OS has a bug where it wont recognize 19041 as bigger. -- !-- This will cause unpackaged activation failures in XAML Islands. -- maxversiontested Id10.0.18362.0/这印证了原文档关于maxversiontested与 1903/18362 版本门的论述非打包激活的 XAML Islands 场景对maxversiontested值极其敏感且不能写成 19041系统 bug 认为 19041 不比 18362 大。3. 告诉 TAEF 使用该清单。TAEF 默认不会加载测试 DLL 的清单需要在测试类上声明ActivationContext属性class SettingsTests { // 告诉 TAEF 在本测试类中把该文件作为 SxS 清单使用。 // 清单里必须包含所有待激活的 C/WinRT 类型 // 否则测试可能以未知原因崩溃——先检查类型是否漏写。 BEGIN_TEST_CLASS(SettingsTests) TEST_CLASS_PROPERTY(LActivationContext, LTerminalApp.Unit.Tests.manifest) END_TEST_CLASS() };完成以上三步后任何实例化自研 WinRT 类型的测试方法都能工作——前提是它不碰 XAML。第三步测试 XAML 类型XAML Islands / Xaml Hosting要在测试里实例化 XAML 控件必须走XAML Hosting API即 XAML Islands让 Win32 上下文可以调用 XAML API。添加 XAML Hosting 代码在测试工程的precomp.h中加入四个头#include winrt/Windows.system.h #include winrt/Windows.Foundation.Collections.h #include winrt/Windows.UI.Xaml.Hosting.h #include windows.ui.xaml.hosting.desktopwindowxamlsource.h如果编译报与GetCurrentTime相关的警告Windows.h的宏定义与 CppWinRT 冲突需要 undef。当前 ut_app/precomp.h第 22–28 行就是这么做的#ifdef GetCurrentTime #undef GetCurrentTime #endif然后在测试类里启动 Xaml Islands。文档建议放在TEST_CLASS_SETUP中只初始化一次并在各测试方法间复用class TabTests { TEST_CLASS_SETUP(ClassSetup) { winrt::init_apartment(winrt::apartment_type::single_threaded); // Initialize the Xaml Hosting Manager _manager winrt::Windows::UI::Xaml::Hosting::WindowsXamlManager::InitializeForCurrentThread(); _source winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource{}; return true; } private: winrt::Windows::UI::Xaml::Hosting::WindowsXamlManager _manager{ nullptr }; winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source{ nullptr }; };编写测试专用 AppxManifest仅有上面的代码还不够。XAML Hosting API 在 Windows 18295 之后要求宿主可执行文件的清单maxversiontested高于该版本而 TAEF 的te.exe没有这样的清单SxS 清单也设置不了它——于是只能让 TAEF 把测试二进制部署成一个临时打包应用来运行并使用我们自己的 AppxManifest。仓库中的实例是 TerminalApp.Unit.Tests.AppxManifest.xml结构如下关键节选Package xmlns:rescap... xmlns... xmlns:uap... IgnorableNamespacesuap Identity NameTerminalApp.Unit.Tests.Package ProcessorArchitectureneutral PublisherCNMicrosoft Corporation, ... Version1.0.0.0 ResourceIden-us / Properties DisplayNameTerminalApp.Unit.Tests.Package Host Process/DisplayName ... /Properties Dependencies TargetDeviceFamily NameWindows.Universal MinVersion10.0.18362.0 MaxVersionTested10.0.26100.0 / PackageDependency NameMicrosoft.VCLibs.140.00.Debug MinVersion14.0.27023.1 Publisher... / PackageDependency NameMicrosoft.VCLibs.140.00.Debug.UWPDesktop MinVersion14.0.27027.1 Publisher... / /Dependencies Applications Application IdTE.ProcessHost ExecutableTE.ProcessHost.exe EntryPointWindows.FullTrustApplication ... /Application /Applications Capabilities rescap:Capability NamerunFullTrust/ /Capabilities Extensions Extension Categorywindows.activatableClass.inProcessServer InProcessServer PathTerminalSettings.dll/Path ActivatableClass ActivatableClassIdMicrosoft.Terminal.Settings.TerminalSettings ThreadingModelboth / ActivatableClass ActivatableClassIdMicrosoft.Terminal.Settings.KeyChord ThreadingModelboth / /InProcessServer /Extension !-- 其余依赖TerminalApp.dll、TerminalConnection.dll、TerminalControl.dll、Microsoft.UI.Xaml.dll同理 -- /Extensions /Package文档对这份文件给出的硬性规则在仓库文件里都得到验证MaxVersionTested必须大于 10.0.18295.0否则 XAML Islands 拒绝激活Application IdTE.ProcessHost ExecutableTE.ProcessHost.exe EntryPointWindows.FullTrustApplication这一行绝不能改——这是 TAEF 激活测试宿主的方式。可能出现「建议改用TE.ProcessHost.UAP.exe」的警告但文档作者实测 UAP 版本不可用Identity、DisplayName等字段可按自己的测试改名因为 TAEF 测试后会自动部署并删除该临时包Extensions块中按 SxS 清单相同思路逐 DLL 列出可激活类型但语法不同InProcessServerActivatableClass。仓库文件头部的注释还给了实用技巧AppxManifest 第 4–18 行类型清单变化时最省事的更新方式是先用 VS 部署一次应用再把生成的appxmanifest.xml里的Extensions复制过来。把 AppxManifest 复制到输出目录并启用AppxManifest 同样需要 binplace 到测试二进制旁边。由于一个项目只能有一个PostBuildEvent文档特别叮嘱不要为每个步骤重复定义MSBuild 只会执行最后一个。当前工程的PostBuildEvent正是「清单 AppxManifest 依赖 DLL」一次性拷贝的形态TerminalApp.UnitTests.vcxproj 第 84–101 行其注释明确列出了三类文件的用途SxS 清单供非打包激活、AppxManifest 供 TAEF 创建临时包、依赖 DLL 供类型激活。最后在测试代码中用两个新属性替换之前的ActivationContextBEGIN_TEST_CLASS(TabTests) TEST_CLASS_PROPERTY(LRunAs, LUAP) TEST_CLASS_PROPERTY(LUAP:AppXManifest, LTerminalApp.Unit.Tests.AppxManifest.xml) END_TEST_CLASS()RunAsUAP让 TAEF 把测试以打包身份运行UAP:AppXManifest指定临时包使用哪份 AppxManifest。文档给出的完整测试类形态TabTestsTEST_CLASS_SETUP初始化WindowsXamlManager与DesktopWindowXamlSourceTryCreateXamlObjects测试方法到这里即打通了从单元测试中创建 XAML 对象的最后一步。使用 Microsoft.UI.Xaml 类型的测试如果项目使用了 MUXMicrosoft.UI.XamlNuGet 包Windows Terminal 确实在 common.nugetversions.props 中引用了Microsoft.UI.Xaml.2.8.4好消息是只要上面的步骤完整执行MUX 类型即可直接调用。原因是 AppxManifest 中已有三行关键配置Dependencies TargetDeviceFamily NameWindows.Universal MinVersion10.0.18362.0 MaxVersionTested10.0.26100.0 / PackageDependency NameMicrosoft.VCLibs.140.00.Debug MinVersion14.0.27023.1 Publisher... / PackageDependency NameMicrosoft.VCLibs.140.00.Debug.UWPDesktop MinVersion14.0.27027.1 Publisher... / /Dependencies缺少这两条 VCLibsPackageDependencyMicrosoft.UI.Xaml.dll将无法加载。仓库中 AppxManifest 的Extensions里列出了Microsoft.UI.Xaml.dll的近百个可激活类型第 78–193 行正是「MUX 类型已完整入清单」的实证。当前仓库的落地形态几个延伸细节对照仓库源码还可以补充原文档之后形成的几条实践经验测试分层ut_app/precomp.h第 34–38 行的注释区分了两个测试工程——TerminalApp.UnitTests在 CI 中运行不能依赖 XAML Islands 与非打包 WinRT 激活与 LocalTests_TerminalApp仅在本地运行的 LocalTests承载那些需要 XAML Islands 的测试。从源码结构看这是因为 CI 环境无法稳定复现打包/非打包激活场景于是把高风险用例隔离到本地测试。测试工程自身的编译细节TerminalApp.UnitTests.vcxproj 第 49–58 行显示测试 DLL 链接时通过/INCLUDE:DllMain强制使用SettingsModelLib内的DllMain否则报 LNK2005 重复定义这是多静态库被链入一个测试 DLL 时的典型冲突。纯 C 测试无需清单像 JsonUtilsTests.cpp、FzfTests.cpp这类只测静态库内普通 C 模板/类的测试完全不需要 SxS 清单与 AppxManifest 那套机制——这正是把实现搬进静态库后获得的最大收益。小结为 C/WinRT XAML Islands 应用搭建 TAEF 单元测试核心是把工程结构改造成「静态库承载实现、DLL 只做包装」再按测试深度分三层递进测试目标需要的机制关键配置纯 C 类静态库 ProjectReference无自研 WinRT 类型非打包SxS 清单 嵌入与 binplaceGenerateManifest/EmbedManifest、TEST_CLASS_PROPERTY(LActivationContext, ...)XAML 类型AppxManifest 临时包 VCLibs 依赖TEST_CLASS_PROPERTY(LRunAs, LUAP)UAP:AppXManifest所有配置均可在当前仓库中对照验证测试工程、SxS 清单、AppxManifest、静态库工程、DLL 包装工程。TAEF 框架的更完整背景另见 doc/TAEF.md。【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表