如何通过NUnit.Engine以独立进程编程运行NUnit测试
我基于.NET 7.0开发了一款C#应用,通过NUnit 3.13.3和NUnit.Engine 3.16.3以编程方式加载并执行单元测试,测试同样基于.NET 7.0开发。
当前测试与应用在同一进程运行,引发以下问题:
- 若应用与测试存在共同依赖,测试会使用应用的依赖版本而非自身的,版本存在差异时会引发问题;
- 当应用尝试重载测试时,测试DLL可被重载,但它的依赖无法重载(无论是否与应用存在共同依赖)。
我了解nunit-console会在独立进程中运行测试,这能解决我的问题,但不知如何通过编程实现这一功能。目前通过ITestRunner runner = TestEngineActivator.CreateInstance().GetRunner(_testPackage);获取的Runner始终在应用进程内运行,无论TestPackage配置何种设置。
我的问题是:如何获取可在独立进程运行测试的Runner?TestPackage需要配置哪些设置?是否需要额外安装NuGet包?
相关代码实现
获取ITestEngine
public TestModel() { _engine = TestEngineActivator.CreateInstance(); _settingsService = new SettingsService(true); _settingsService.LoadSettings(); _engine.InternalTraceLevel = InternalTraceLevel.Off; }
加载测试
public async Task<XmlNode?> LoadTestsAsync(string dllTestFile) { LoadedTestFile = dllTestFile; _testPackage = new TestPackage(LoadedTestFile); _runner = _engine.GetRunner(_testPackage); var nodes = await Task.Run(() => _runner.Explore(TestFilter.Empty)); XmlWriter writer = XmlWriter.Create("tests.xml", new XmlWriterSettings() { Indent = true}); nodes.WriteContentTo(writer); writer.Flush(); writer.Dispose(); return nodes; }
运行测试
public void Run(TestNode testNode) { var filterService = _engine?.Services.GetService<ITestFilterService>(); var builder = filterService?.GetTestFilterBuilder(); CheckVirtualAndAddChildren(builder, testNode); _runner?.RunAsync(_events, builder?.GetFilter()); }
重载测试
public async Task<XmlNode?> ReloadAsync() { try { _runner?.Unload(); } catch (NUnitEngineUnloadException) {} _runner?.Dispose(); _runner = _engine.GetRunner(_testPackage); var nodes = await Task.Run(() => _runner?.Explore(TestFilter.Empty)); XmlWriter writer = XmlWriter.Create("tests.xml", new XmlWriterSettings() { Indent = true}); nodes?.WriteContentTo(writer); writer.Flush(); writer.Dispose(); return nodes; }
ITestRunner.Explore返回的XML
<?xml version="1.0" encoding="utf-8"?> <test-suite type="Assembly" id="0-1007" name="NUnitTests.dll" fullname="H:/MyApplication/sample/NUnitDemo/NUnitTests/bin/Debug/net7.0/NUnitTests.dll" runstate="Runnable" testcasecount="4"> <environment framework-version="3.13.3.0" clr-version="7.0.5" os-version="Microsoft Windows 10.0.19045" platform="Win32NT" cwd="H:\MyApplication\MyApplication\bin\Debug\net7.0" machine-name="XXXX" user="myself" user-domain="XXXX" culture="de-DE" uiculture="de-DE" os-architecture="x64" /> <settings> <setting name="NumberOfTestWorkers" value="16" /> </settings> <properties> <property name="_PID" value="29016" /> <property name="_APPDOMAIN" value="MyApplication" /> </properties> <test-suite type="TestSuite" id="0-1008" name="NUnitTests" fullname="NUnitTests" runstate="Runnable" testcasecount="4"> <test-suite type="TestFixture" id="0-1000" name="DemoTests" fullname="NUnitTests.DemoTests" classname="NUnitTests.DemoTests" runstate="Runnable" testcasecount="4"> <test-suite type="ParameterizedMethod" id="0-1006" name="BaseDependency" fullname="NUnitTests.DemoTests.BaseDependency" classname="NUnitTests.DemoTests" runstate="Runnable" testcasecount="2"> <test-case id="0-1004" name="BaseDependency(Cycle #1)" fullname="NUnitTests.DemoTests.BaseDependency(Cycle #1)" methodname="BaseDependency" classname="NUnitTests.DemoTests" runstate="Runnable" seed="1298419094" /> <test-case id="0-1005" name="BaseDependency(Cycle #2)" fullname="NUnitTests.DemoTests.BaseDependency(Cycle #2)" methodname="BaseDependency" classname="NUnitTests.DemoTests" runstate="Runnable" seed="2074295330" /> </test-suite> <test-suite type="ParameterizedMethod" id="0-1003" name="NoBaseDependency" fullname="NUnitTests.DemoTests.NoBaseDependency" classname="NUnitTests.DemoTests" runstate="Runnable" testcasecount="2"> <test-case id="0-1001" name="NoBaseDependency(Cycle #1)" fullname="NUnitTests.DemoTests.NoBaseDependency(Cycle #1)" methodname="NoBaseDependency" classname="NUnitTests.DemoTests" runstate="Runnable" seed="1168845109" /> <test-case id="0-1002" name="NoBaseDependency(Cycle #2)" fullname="NUnitTests.DemoTests.NoBaseDependency(Cycle #2)" methodname="NoBaseDependency" classname="NUnitTests.DemoTests" runstate="Runnable" seed="64791598" /> </test-suite> </test-suite> </test-suite> </test-suite>
要让NUnit Engine在独立进程中运行测试,只需配置TestPackage启用进程隔离,引擎会自动返回进程外Runner,具体步骤如下:
1. 配置TestPackage的进程隔离参数
在创建TestPackage后添加以下设置:
_testPackage = new TestPackage(LoadedTestFile); // 启用独立进程模式:Separate为每个测试包开一个进程,Multiple为每个程序集开一个进程 _testPackage.AddSetting("ProcessModel", "Separate"); // 明确指定测试运行的.NET版本,避免版本兼容问题 _testPackage.AddSetting("RuntimeFramework", "net7.0");
2. 无需额外修改Runner获取逻辑
当配置了ProcessModel=Separate后,_engine.GetRunner(_testPackage)会自动返回进程外Runner(ProcessRunner),测试将在独立进程中执行,自动解决依赖版本冲突和DLL重载问题。
3. NuGet包检查
你当前使用的NUnit.Engine 3.16.3已包含进程隔离功能,无需额外安装其他包。只需确保测试项目的NUnit版本(3.13.3)与引擎版本兼容即可,当前版本组合是兼容的。
修改后的加载测试代码
public async Task<XmlNode?> LoadTestsAsync(string dllTestFile) { LoadedTestFile = dllTestFile; _testPackage = new TestPackage(LoadedTestFile); // 添加进程隔离配置 _testPackage.AddSetting("ProcessModel", "Separate"); _testPackage.AddSetting("RuntimeFramework", "net7.0"); _runner = _engine.GetRunner(_testPackage); var nodes = await Task.Run(() => _runner.Explore(TestFilter.Empty)); XmlWriter writer = XmlWriter.Create("tests.xml", new XmlWriterSettings() { Indent = true}); nodes.WriteContentTo(writer); writer.Flush(); writer.Dispose(); return nodes; }
验证进程隔离是否生效
运行测试后,查看ITestRunner.Explore返回的XML中的_PID属性,若每次运行测试的PID不同,说明测试在独立进程中执行;同时_APPDOMAIN会显示测试进程的AppDomain名称,而非主应用的。
重载测试的优化
使用进程隔离后,重载测试时只需正常调用_runner.Unload()和重新创建Runner即可,测试进程会被销毁并重新启动,所有依赖DLL都会被重新加载,彻底解决之前的重载问题。
内容的提问来源于stack exchange,提问作者user23

