当前位置 博文首页 > dino.c:[WPF] 在单元测试中使用 Prism 的 EventAggregator,订

    dino.c:[WPF] 在单元测试中使用 Prism 的 EventAggregator,订

    作者:dino.c 时间:2021-01-30 18:16

    1. 问题

    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {
            ContainerLocator.Container.Resolve<TestViewModel>();
        }
    }
    
    public class TestViewModel
    {
        public TestViewModel(IEventAggregator eventAggregator)
        {
            var testEvent = eventAggregator.GetEvent<TestEvent>();
            testEvent.Subscribe(() => { }, ThreadOption.UIThread);
        }
    }
    
    public class TestEvent : PubSubEvent
    {
    
    }
    

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

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

    2. 原因

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

    switch (threadOption)
    {
        case ThreadOption.PublisherThread:
            subscription = new EventSubscription(actionReference);
            break;
        case ThreadOption.BackgroundThread:
            subscription = new BackgroundEventSubscription(actionReference);
            break;
        case ThreadOption.UIThread:
            if (SynchronizationContext == null) throw new InvalidOperationException(Resources.EventAggregatorNotConstructedOnUIThread);
            subscription = new DispatcherEventSubscription(actionReference, SynchronizationContext);
            break;
        default:
            subscription = new EventSubscription(actionReference);
            break;
    

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

    private readonly SynchronizationContext syncContext = SynchronizationContext.Current;
    
    public TEventType GetEvent<TEventType>() where TEventType : EventBase, new()
    {
        lock (events)
        {
            EventBase existingEvent = null;
    
            if (!events.TryGetValue(typeof(TEventType), out existingEvent))
            {
                TEventType newEvent = new TEventType();
                newEvent.SynchronizationContext = syncContext;
                events[typeof(TEventType)] = newEvent;
    
                return newEvent;
            }
            else
            {
                return (TEventType)existingEvent;
            }
        }
    }
    

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

    3. 解决方案

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

    private readonly SynchronizationContext syncContext = SynchronizationContext.Current;
    

    替换成这句:

    private readonly SynchronizationContext syncContext = new SynchronizationContext();
    

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

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

    4. 最后

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

    [TestClass]
    public abstract class TestInitializerBase
    {
        public void Initialize()
        {
            ContainerLocator.SetContainerExtension(() => new UnityContainerExtension());
            ContainerExtension = ContainerLocator.Current;
    
            ContainerExtension.RegisterSingleton<IDialogService, DialogService>();
            ContainerExtension.RegisterSingleton<IModuleInitializer, ModuleInitializer>();
            ContainerExtension.RegisterSingleton<IModuleManager, ModuleManager>();
            ContainerExtension.RegisterSingleton<RegionAdapterMappings>();
            ContainerExtension.RegisterSingleton<IRegionManager, RegionManager>();
            ContainerExtension.RegisterSingleton<IRegionNavigationContentLoader, RegionNavigationContentLoader>();
    
            ContainerExtension.RegisterSingleton<IEventAggregator, EventAggregator>();
    
            ContainerExtension.RegisterSingleton<IRegionViewRegistry, RegionViewRegistry>();
            ContainerExtension.RegisterSingleton<IRegionBehaviorFactory, RegionBehaviorFactory>();
            ContainerExtension.Register<IRegionNavigationJournalEntry, RegionNavigationJournalEntry>();
            ContainerExtension.Register<IRegionNavigationJournal, RegionNavigationJournal>();
            ContainerExtension.Register<IRegionNavigationService, RegionNavigationService>();
    
          
            RegisterRequiredTypes(ContainerExtension);
    
        }
    
        public IContainerExtension ContainerExtension { get; private set; }
    
        protected abstract void RegisterRequiredTypes(IContainerRegistry containerRegistry);
    }
    
    public class TestInitializer : TestInitializerBase
    {
        [AssemblyInitialize]
        public static void InitializeAseemble(TestContext testContext)
        {
            var testInitializer = new TestInitializer();
            testInitializer.Initialize();
        }
    
        protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterSingleton<IEventAggregator, MyEventAggregator>();
        }
    }
    

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

    bk