Blazor bUnit测试:Mock ProtectedSessionStorage返回值重复问题排查
解决bUnit中Mock ProtectedSessionStorage的key值覆盖问题
问题场景
在使用bUnit为Blazor项目编写单元测试时,Mock ProtectedSessionStorage 后出现不同key返回相同值的问题:
- 调用
SetCountry和SetLanguage设置两个不同key的存储值后,获取_language能得到正确的1,但_countryId也返回1; - 调整
Set方法的调用顺序后,所有key都会返回最后一次设置的值,明显存在逻辑覆盖问题。
当前Mock实现、注入方式及组件获取值的代码如下:
当前Mock实现代码
public class MockDataProtectionProvider { public Mock<IDataProtectionProvider> Mock = new Mock<IDataProtectionProvider>(); private Mock<IDataProtector>? mockDataProtector; public void SetAll(BunitJSInterop JSInterop) { SetCountry(JSInterop); SetLanguage(JSInterop); } public void SetLanguage(BunitJSInterop JSInterop) { Set("1", "_language", JSInterop); } public void SetCountry(BunitJSInterop JSInterop) { Set("55BBF40F-3693-478E-948A-FB2321DB7771", "_countryId", JSInterop); } private void Set(string value, string key, BunitJSInterop JSInterop) { mockDataProtector = new Mock<IDataProtector>(); string valueJson = JsonSerializer.Serialize(value); byte[] valueBytes = Encoding.UTF8.GetBytes(valueJson); byte[] protectedValueBytes = mockDataProtector.Object.Protect(valueBytes); string base64valueJson = Convert.ToBase64String(protectedValueBytes); mockDataProtector.Setup(sut => sut.Protect(It.IsAny<byte[]>())).Returns(protectedValueBytes); mockDataProtector.Setup(sut => sut.Unprotect(It.IsAny<byte[]>())).Returns(valueBytes); Mock.Setup(s => s.CreateProtector(It.IsAny<string>())).Returns(mockDataProtector.Object); JSInterop.Setup<String>("sessionStorage.getItem", key).SetResult(base64valueJson); } }
注入方式
//Prepare JSInterop to get set data into SessionStorage DataProtectionProviderMock.SetAll(JSInterop); _protectedSessionStorage = new ProtectedSessionStorage( JSInterop.JSRuntime, DataProtectionProviderMock.Mock.Object); Services.AddSingleton(_protectedSessionStorage);
组件获取值代码
protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { var languageIdResult = await Storage.GetAsync<int>(nameof(_language)); var countryIdResult = await Storage.GetAsync<Guid>(nameof(_countryId)); _language = languageIdResult.Success ? languageIdResult.Value : default; _countryId = countryIdResult.Success ? countryIdResult.Value : default; } }
问题根源
- Mock对象被重复覆盖:每次调用
Set方法时,都会重新创建mockDataProtector实例,并且重新设置IDataProtectionProvider.CreateProtector的返回值,最后一次创建的mockDataProtector会覆盖之前所有的设置。 - Unprotect逻辑无区分:所有
Unprotect调用都被硬编码返回最后一次设置的valueBytes,不管输入的加密字节对应哪个key,都会返回同一个原始值。
修正方案
核心思路是:维护每个key对应的加密/原始字节映射,让Mock的Unprotect方法能根据输入的加密字节返回对应的原始值,同时避免重复覆盖Mock对象。
修正后的Mock实现代码
public class MockDataProtectionProvider { public Mock<IDataProtectionProvider> Mock { get; } = new Mock<IDataProtectionProvider>(); private Mock<IDataProtector> _mockDataProtector; // 存储加密字节与原始字节的映射 private Dictionary<byte[], byte[]> _protectionMap = new Dictionary<byte[], byte[]>(new ByteArrayComparer()); public MockDataProtectionProvider() { _mockDataProtector = new Mock<IDataProtector>(); // 只设置一次CreateProtector,避免重复覆盖 Mock.Setup(s => s.CreateProtector(It.IsAny<string>())).Returns(_mockDataProtector.Object); // 设置Protect逻辑:生成唯一的加密字节(这里用Guid生成模拟,确保每个值的加密结果不同) _mockDataProtector.Setup(sut => sut.Protect(It.IsAny<byte[]>())) .Returns<byte[]>(originalBytes => { var protectedBytes = Guid.NewGuid().ToByteArray(); _protectionMap[protectedBytes] = originalBytes; return protectedBytes; }); // 设置Unprotect逻辑:根据输入的加密字节返回对应的原始字节 _mockDataProtector.Setup(sut => sut.Unprotect(It.IsAny<byte[]>())) .Returns<byte[]>(protectedBytes => { _protectionMap.TryGetValue(protectedBytes, out var originalBytes); return originalBytes ?? Array.Empty<byte>(); }); } public void SetAll(BunitJSInterop JSInterop) { SetCountry(JSInterop); SetLanguage(JSInterop); } public void SetLanguage(BunitJSInterop JSInterop) { Set("1", "_language", JSInterop); } public void SetCountry(BunitJSInterop JSInterop) { Set("55BBF40F-3693-478E-948A-FB2321DB7771", "_countryId", JSInterop); } private void Set(string value, string key, BunitJSInterop JSInterop) { string valueJson = JsonSerializer.Serialize(value); byte[] valueBytes = Encoding.UTF8.GetBytes(valueJson); // 调用Mock的Protect生成加密字节 byte[] protectedValueBytes = _mockDataProtector.Object.Protect(valueBytes); string base64valueJson = Convert.ToBase64String(protectedValueBytes); // 为当前key设置SessionStorage的返回值 JSInterop.Setup<string>("sessionStorage.getItem", key).SetResult(base64valueJson); } // 自定义字节数组比较器,用于Dictionary的键比较 private class ByteArrayComparer : IEqualityComparer<byte[]> { public bool Equals(byte[]? x, byte[]? y) { if (x == null || y == null) return x == y; return StructuralComparisons.StructuralEqualityComparer.Equals(x, y); } public int GetHashCode(byte[] obj) { return obj.Aggregate(0, (hash, b) => hash ^ b.GetHashCode()); } } }
关键改动说明
- 初始化逻辑集中:在构造函数中一次性完成
IDataProtectionProvider和IDataProtector的Mock设置,避免重复覆盖。 - 映射存储加密关系:用
Dictionary存储每个原始字节对应的加密字节,确保Unprotect能精准返回对应值。 - 动态生成加密字节:用
Guid.NewGuid()模拟加密后的唯一字节数组,保证不同key的加密结果不重复。 - 自定义字节数组比较器:解决字节数组作为字典键无法正确比较的问题。
验证效果
使用修正后的MockDataProtectionProvider,注入方式保持不变,此时组件中调用GetAsync<int>("_language")会返回1,调用GetAsync<Guid>("_countryId")会返回正确的55BBF40F-3693-478E-948A-FB2321DB7771,不会再出现值覆盖的问题。
内容的提问来源于stack exchange,提问作者João Estrela
相关产品推荐
相关产品推荐

