Visual Studio 內建測試工具對 WPF 專案支援有限(VS2005 不支援,VS2008 亦有限制),導致開發者難以對 UI 組件編寫單元測試。本文介紹如何使用 NUnit 搭配自定義 CrossThreadTestRunner 解決「呼叫線程必須為 STA」的經典問題,實現 WPF UI 組件的有效測試。
與 .NET 2.0 時代無異,直接使用 NUnit 標準寫法即可,無任何阻礙。
[TestFixture]
public class ClassTest {
[Test]
public void TestRun() {
var obj = new ClassLibrary1.Class1();
double expected = 9;
double result = obj.GetSomeValue(3);
Assert.AreEqual(expected, result);
}
}
直接實例化 Window 或 UserControl 會拋出異常:
"The calling thread must be STA, because many UI components require this"
核心思路:創建一個獨立的 STA 線程,在該線程中執行測試代理,並正確傳遞異常堆疊資訊。
using System;
using System.Threading;
using System.Security.Permissions;
using System.Reflection;
namespace TestUnit {
public class CrossThreadTestRunner {
private Exception lastException;
public void RunInMTA(ThreadStart userDelegate) {
Run(userDelegate, ApartmentState.MTA);
}
public void RunInSTA(ThreadStart userDelegate) {
Run(userDelegate, ApartmentState.STA);
}
private void Run(ThreadStart userDelegate, ApartmentState apartmentState) {
lastException = null;
Thread thread = new Thread(delegate() {
try {
userDelegate.Invoke();
} catch (Exception e) {
lastException = e;
}
});
thread.SetApartmentState(apartmentState);
thread.Start();
thread.Join();
if (ExceptionWasThrown())
ThrowExceptionPreservingStack(lastException);
}
private bool ExceptionWasThrown() {
return lastException != null;
}
[ReflectionPermission(SecurityAction.Demand)]
private static void ThrowExceptionPreservingStack(Exception exception) {
FieldInfo remoteStackTraceString = typeof(Exception).GetField(
"_remoteStackTraceString",
BindingFlags.Instance | BindingFlags.NonPublic);
remoteStackTraceString.SetValue(exception, exception.StackTrace + Environment.NewLine);
throw exception;
}
}
}
[TestFixture]
public class ClassTest {
[Test]
public void TestRun() {
CrossThreadTestRunner runner = new CrossThreadTestRunner();
runner.RunInSTA(delegate {
Console.WriteLine(Thread.CurrentThread.GetApartmentState());
var obj = new WindowsApplication1.Window1();
double expected = 9;
double result = obj.GetSomeValue(3);
Assert.AreEqual(expected, result);
});
}
}
nunit.framework.dll。[TestFixture],測試方法標註 [Test]。nunit.exe (或 nunit-console.exe) 執行。WPF UI 組件依賴單執行緒單元 (STA) 模型。NUnit 預設在 MTA 線程中運行測試。CrossThreadTestRunner 通過顯式創建 STA 線程並同步等待 (Join),確保 UI 物件在正確的線程模型中建構與銷毀。反射修改 _remoteStackTraceString 私有欄位可保留原始堆疊追蹤,方便除錯定位。
此方案雖屬「權宜之計」,但在缺乏原生 WPF 測試支援的早期版本中極具實用價值。現代 .NET Core/.NET 5+ 專案建議改用 Microsoft.VisualStudio.TestPlatform 或 xUnit 搭配 WpfTestRunner 等原生支援方案。