You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
    }
}

问题根源

  1. Mock对象被重复覆盖:每次调用Set方法时,都会重新创建mockDataProtector实例,并且重新设置IDataProtectionProvider.CreateProtector的返回值,最后一次创建的mockDataProtector会覆盖之前所有的设置。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 16:28:09