1. [TestClass]
  2. public class UnitTest1
  3. {
  4. [TestMethod]
  5. public void TestMethod1()
  6. {
  7. ContainerLocator.Container.Resolve<TestViewModel>();
  8. }
  9. }
  10. public class TestViewModel
  11. {
  12. public TestViewModel(IEventAggregator eventAggregator)
  13. {
  14. var testEvent = eventAggregator.GetEvent<TestEvent>();
  15. testEvent.Subscribe(() => { }, ThreadOption.UIThread);
  16. }
  17. }
  18. public class TestEvent : PubSubEvent
  19. {
  20. }

上面是一段使用了 Prism 的单元测试,它主要的逻辑是在 EventAggregator 中订阅了 TestEvent,当接收到消息后在 UI 线程上执行后续的逻辑。这种代码在正常程序中没有问题,但在单元测试中会报错:

System.InvalidOperationException: To use the UIThread option for subscribing, the EventAggregator must be constructed on the UI thread.

翻翻源码,可以发现这个 Exception 在 PubSubEventSubscribe 函数中抛出:

  1. switch (threadOption)
  2. {
  3. case ThreadOption.PublisherThread:
  4. subscription = new EventSubscription(actionReference);
  5. break;
  6. case ThreadOption.BackgroundThread:
  7. subscription = new BackgroundEventSubscription(actionReference);
  8. break;
  9. case ThreadOption.UIThread:
  10. if (SynchronizationContext == null) throw new InvalidOperationException(Resources.EventAggregatorNotConstructedOnUIThread);
  11. subscription = new DispatcherEventSubscription(actionReference, SynchronizationContext);
  12. break;
  13. default:
  14. subscription = new EventSubscription(actionReference);
  15. break;

SynchronizationContext 为 null 时就会判断当前不在 UI 线程,然后抛出 Exception。而 SynchronizationContext 又是在 EventAggregator 中赋值:

  1. private readonly SynchronizationContext syncContext = SynchronizationContext.Current;
  2. public TEventType GetEvent<TEventType>() where TEventType : EventBase, new()
  3. {
  4. lock (events)
  5. {
  6. EventBase existingEvent = null;
  7. if (!events.TryGetValue(typeof(TEventType), out existingEvent))
  8. {
  9. TEventType newEvent = new TEventType();
  10. newEvent.SynchronizationContext = syncContext;
  11. events[typeof(TEventType)] = newEvent;
  12. return newEvent;
  13. }
  14. else
  15. {
  16. return (TEventType)existingEvent;
  17. }
  18. }
  19. }

问题就出在 SynchronizationContext.Current 这里。这个属性用于获取当前线程的同步上下文。不是每一个线程都有一个 SynchronizationContext 对象。一个总是有 SynchronizationContext 对象的是UI线程。由于单元测试并不是运行在 UI 线程,所以这个属性在单元测试中一直为 null。

现在我们知道问题原因了,解决方案也很简单,只要自定义一个 EventAggregator,源码全部照抄,但是把这句:

  1. private readonly SynchronizationContext syncContext = SynchronizationContext.Current;

替换成这句:

  1. private readonly SynchronizationContext syncContext = new SynchronizationContext();

就不会出现 PubSubEvent 中 SynchronizationContext 等于 null 的情况了。然后再把这个类注册到容器中作为 IEventAggregator:

  1. ContainerLocator.Current.RegisterSingleton<IEventAggregator, MyEventAggregator>();

根据单元测试项目的结构,容器的初始化会有不同的方式,如果想尽量模仿 PrismApplication 的话可以参考 PrismApplicationBasePrismInitializationExtensions 写一个初始化类,大概差不多这样(简化了部分代码):

  1. [TestClass]
  2. public abstract class TestInitializerBase
  3. {
  4. public void Initialize()
  5. {
  6. ContainerLocator.SetContainerExtension(() => new UnityContainerExtension());
  7. ContainerExtension = ContainerLocator.Current;
  8. ContainerExtension.RegisterSingleton<IDialogService, DialogService>();
  9. ContainerExtension.RegisterSingleton<IModuleInitializer, ModuleInitializer>();
  10. ContainerExtension.RegisterSingleton<IModuleManager, ModuleManager>();
  11. ContainerExtension.RegisterSingleton<RegionAdapterMappings>();
  12. ContainerExtension.RegisterSingleton<IRegionManager, RegionManager>();
  13. ContainerExtension.RegisterSingleton<IRegionNavigationContentLoader, RegionNavigationContentLoader>();
  14. ContainerExtension.RegisterSingleton<IEventAggregator, EventAggregator>();
  15. ContainerExtension.RegisterSingleton<IRegionViewRegistry, RegionViewRegistry>();
  16. ContainerExtension.RegisterSingleton<IRegionBehaviorFactory, RegionBehaviorFactory>();
  17. ContainerExtension.Register<IRegionNavigationJournalEntry, RegionNavigationJournalEntry>();
  18. ContainerExtension.Register<IRegionNavigationJournal, RegionNavigationJournal>();
  19. ContainerExtension.Register<IRegionNavigationService, RegionNavigationService>();
  20. RegisterRequiredTypes(ContainerExtension);
  21. }
  22. public IContainerExtension ContainerExtension { get; private set; }
  23. protected abstract void RegisterRequiredTypes(IContainerRegistry containerRegistry);
  24. }
  25. public class TestInitializer : TestInitializerBase
  26. {
  27. [AssemblyInitialize]
  28. public static void InitializeAseemble(TestContext testContext)
  29. {
  30. var testInitializer = new TestInitializer();
  31. testInitializer.Initialize();
  32. }
  33. protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry)
  34. {
  35. containerRegistry.RegisterSingleton<IEventAggregator, MyEventAggregator>();
  36. }
  37. }

这样在 TestInitializer 中可以注册各种方便单元测试的伪对象。

版权声明:本文为dino623原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/dino623/p/ThreadOption_UIThread_in_unittest_throws_exception.html