Blazor单元测试中ProtectedSessionStorage依赖注入失败问题
Bunit测试Blazor组件时ProtectedSessionStorage依赖注入错误
问题现象
使用Bunit对Blazor的Assets组件进行单元测试时,执行AssetTableTest构造函数中的Context.RenderComponent<Assets>()步骤时,抛出System.InvalidOperationException,错误提示:
激活'Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.ProtectedSessionStorage'时无法解析类型'Microsoft.AspNetCore.DataProtection.IDataProtectionProvider'的服务。
异常堆栈信息
System.InvalidOperationException HResult=0x80131509 Message=Unable to resolve service for type 'Microsoft.AspNetCore.DataProtection.IDataProtectionProvider' while attempting to activate 'Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.ProtectedSessionStorage'. Source=Microsoft.Extensions.DependencyInjection StackTrace: at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, Type serviceType, Type implementationType, CallSiteChain callSiteChain) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain, Int32 slot) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(Type serviceType, CallSiteChain callSiteChain) at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(Type serviceType) at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType) at Bunit.TestServiceProvider.GetServiceInternal(Type serviceType) at Bunit.TestServiceProvider.GetService(Type serviceType) at Microsoft.AspNetCore.Components.ComponentFactory.<>c__DisplayClass7_0.g__Initialize|1(IServiceProvider serviceProvider, IComponent component) at Microsoft.AspNetCore.Components.ComponentFactory.PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance) at Microsoft.AspNetCore.Components.ComponentFactory.InstantiateComponent(IServiceProvider serviceProvider, Type componentType) at Microsoft.AspNetCore.Components.RenderTree.Renderer.InstantiateChildComponentOnFrame(RenderTreeFrame& frame, Int32 parentComponentId) at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewComponentFrame(DiffContext& diffContext, Int32 frameIndex) at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewSubtree(DiffContext& diffContext, Int32 frameIndex) at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InsertNewFrame(DiffContext& diffContext, Int32 newFrameIndex) at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl) at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.ComputeDiff(Renderer renderer, RenderBatchBuilder batchBuilder, Int32 componentId, ArrayRange`1 oldTree, ArrayRange`1 newTree) at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException) at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue() --- End of stack trace from previous location --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at Bunit.Rendering.TestRenderer.AssertNoUnhandledExceptions() at Bunit.Rendering.TestRenderer.Render[TResult](RenderFragment renderFragment, Func`2 activator) at Bunit.Rendering.TestRenderer.RenderFragment(RenderFragment renderFragment) at Bunit.Extensions.TestContextBaseRenderExtensions.RenderInsideRenderTree(TestContextBase testContext, RenderFragment renderFragment) at Bunit.Extensions.TestContextBaseRenderExtensions.RenderInsideRenderTree[TComponent](TestContextBase testContext, RenderFragment renderFragment) at Bunit.TestContext.Render[TComponent](RenderFragment renderFragment) at Bunit.TestContext.RenderComponent[TComponent](ComponentParameter[] parameters) at ConfigWebappUnitTesting.AssetTableTest..ctor() in D:\Gleason Projects\ConfigurationWebAppBlazor\ConfigWebappUnitTesting\AssetTableTest.cs:line 19 at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions)
测试代码
public AssetTableTest() { // Act var session = Context.Services.GetService<FakeSignOutSessionStateManager>(); var page = Context.Services.GetService<FakeNavigationManager>(); page.NavigateTo(page.BaseUri.Remove(page.BaseUri.Length - 1, 1) + Configuration["Session"]); cut = Context.RenderComponent<Assets>(); //**错误发生在此处** //cut = Context.RenderComponent<Assets>(x => //x.Add(p => p.Session, Configuration["Session"]) //); //WaitForAssertion cut.WaitForAssertion(() => { }, TimeSpan.FromSeconds(5)); cut.WaitForState(() => (cut.Markup.Contains("tbody")), TimeSpan.FromSeconds(10)); cut.WaitForState(() => (!cut.Find("tbody").TextContent.Contains("Loading")), TimeSpan.FromSeconds(50)); tableBody = cut.Find("tbody"); }
解决方案
问题根源是Bunit的TestContext默认没有注册IDataProtectionProvider服务,而ProtectedSessionStorage依赖该服务才能被激活。需要在测试初始化时手动注册所需服务,有两种常用方式:
方式1:注册模拟的IDataProtectionProvider
使用Moq等模拟库创建IDataProtectionProvider的实例并注册到服务容器:
using Moq; public AssetTableTest() { // 注册模拟的IDataProtectionProvider var mockDataProtection = new Mock<IDataProtectionProvider>(); Context.Services.AddSingleton(mockDataProtection.Object); // 原有测试代码 var session = Context.Services.GetService<FakeSignOutSessionStateManager>(); var page = Context.Services.GetService<FakeNavigationManager>(); page.NavigateTo(page.BaseUri.Remove(page.BaseUri.Length - 1, 1) + Configuration["Session"]); cut = Context.RenderComponent<Assets>(); // ...后续断言代码 }
方式2:替换ProtectedSessionStorage为自定义模拟实现
如果组件实际使用了ProtectedSessionStorage的方法(如GetAsync、SetAsync),可以创建一个模拟实现并注册:
// 自定义模拟ProtectedSessionStorage public class MockProtectedSessionStorage : ProtectedSessionStorage { public MockProtectedSessionStorage(IDataProtectionProvider dataProtectionProvider) : base(dataProtectionProvider) { } // 重写需要用到的方法,返回测试所需数据 public override Task<ProtectedBrowserStorageResult<T>> GetAsync<T>(string key) { // 示例:返回成功结果,可根据测试需求修改返回值 return Task.FromResult(new ProtectedBrowserStorageResult<T>(true, default(T))); } public override Task SetAsync<T>(string key, T value) { return Task.CompletedTask; } } // 在测试中注册 public AssetTableTest() { var mockDataProtection = new Mock<IDataProtectionProvider>(); Context.Services.AddSingleton<IProtectedSessionStorage>(new MockProtectedSessionStorage(mockDataProtection.Object)); // 原有测试代码 var session = Context.Services.GetService<FakeSignOutSessionStateManager>(); var page = Context.Services.GetService<FakeNavigationManager>(); page.NavigateTo(page.BaseUri.Remove(page.BaseUri.Length - 1, 1) + Configuration["Session"]); cut = Context.RenderComponent<Assets>(); // ...后续断言代码 }
选择哪种方式取决于组件是否实际调用了ProtectedSessionStorage的方法:如果只是注入但未使用,方式1足够;如果需要验证存储操作或返回特定数据,方式2更合适。
内容的提问来源于stack exchange,提问作者Chethan D V
相关产品推荐
相关产品推荐

