[WPF] 在单元测试中使用 Prism 的 EventAggregator,订阅到 ThreadOption.UIThread 会报错

此流非彼流——Stream详解

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 在 PubSubEventSubscribe 函数中抛出:

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。

记忆中的像素块褪色了吗?用开源的体素编辑器重新做个 3D 的吧!

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 的话可以参考 PrismApplicationBasePrismInitializationExtensions 写一个初始化类,大概差不多这样(简化了部分代码):

[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 中可以注册各种方便单元测试的伪对象。

这难道不是.NET5 的bug? 在线求锤?

给TA买糖
共{{data.count}}人
人已赞赏
经验教程

EF Core 6.0的新计划

2021-1-20 8:53:00

经验教程

此流非彼流——Stream详解

2021-1-20 9:17:00

⚠️
免责声明:根据《计算机软件保护条例》第十七条规定“为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬。”您需知晓本站所有内容资源均来源于网络,仅供用户交流学习与研究使用,版权归属原版权方所有,版权争议与本站无关,用户本人下载后不能用作商业或非法用途,需在24个小时之内从您的电脑中彻底删除上述内容,否则后果均由用户承担责任;如果您访问和下载此文件,表示您同意只将此文件用于参考、学习而非其他用途,否则一切后果请您自行承担,如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。 本站为个人博客非盈利性站点,所有软件信息均来自网络,所有资源仅供学习参考研究目的,并不贩卖软件,不存在任何商业目的及用途,网站会员捐赠是您喜欢本站而产生的赞助支持行为,仅为维持服务器的开支与维护,全凭自愿无任何强求。本站部份代码及教程来源于互联网,仅供网友学习交流,若您喜欢本文可附上原文链接随意转载。
无意侵害您的权益,请发送邮件至 momeis6@qq.com 或点击右侧 私信:momeis 反馈,我们将尽快处理。
0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
今日签到
有新私信 私信列表
搜索