如何Mock返回类型含只读属性与内部构造函数的第三方接口?
解决Moq模拟EventStore的StreamEventsSlice返回值问题
我太懂这种被第三方库的封闭设计卡壳的感觉了!你遇到的问题是EventStore的StreamEventsSlice因为内部构造函数+只读字段的设计,没法直接用常规方式实例化来给Moq做返回值,下面给你几个实用的解决方案:
方案1:用反射绕过访问限制实例化
既然StreamEventsSlice的构造函数是internal的,我们可以用反射直接调用它的构造函数,传入需要的参数来创建实例(毕竟只读字段都是在构造时赋值的)。
代码示例:
// 先获取StreamEventsSlice的内部构造函数,注意要和实际构造函数的参数列表匹配 var constructor = typeof(StreamEventsSlice) .GetConstructor( BindingFlags.Instance | BindingFlags.NonPublic, null, // 这里替换成StreamEventsSlice实际的构造参数类型,比如string, long, int, bool等 new[] { typeof(string), typeof(long), typeof(int), typeof(bool), typeof(SliceReadStatus), typeof(List<ResolvedEvent>), typeof(long), typeof(long), typeof(bool) }, null); // 调用构造函数创建实例,传入你需要的测试数据 var testSlice = constructor.Invoke(new object[] { "test-stream", // Stream字段值 0, // Start 10, // Count true, // ResolveLinkTos SliceReadStatus.Success, // 读取状态 new List<ResolvedEvent>(), // 事件列表 0, // NextEventNumber 9, // LastEventNumber false // IsEndOfStream }) as StreamEventsSlice; // 配置Moq返回这个实例 var connectionMock = new Mock<IEventStoreConnection>(); connectionMock.Setup(s => s.ReadStreamEventsForwardAsync( It.IsAny<string>(), It.IsAny<long>(), It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<UserCredentials>())) .ReturnsAsync(testSlice);
小提示:你需要对照EventStore版本里
StreamEventsSlice的实际构造参数来调整上面的类型数组和参数值,避免构造失败。
方案2:封装适配层解耦第三方依赖
如果不想用反射这种“hack”手法,更优雅的方式是在业务代码和EventStore ClientAPI之间加一层自己的接口,屏蔽掉第三方库的具体类型。
步骤1:定义自己的接口和DTO
// 自定义的读取接口 public interface IEventStoreStreamReader { Task<MyStreamEventsSlice> ReadForwardAsync( string streamId, long startPosition, int eventCount, bool resolveLinks, UserCredentials credentials = null); } // 自定义的DTO,完全由你控制 public class MyStreamEventsSlice { public string StreamId { get; set; } public bool IsEndOfStream { get; set; } public List<MyResolvedEvent> Events { get; set; } = new(); // 其他你需要的属性 } // 自定义事件DTO(可选,根据需求调整) public class MyResolvedEvent { // 业务需要的事件属性 }
步骤2:实现适配器调用真实EventStore
public class EventStoreStreamReaderAdapter : IEventStoreStreamReader { private readonly IEventStoreConnection _eventStoreConnection; public EventStoreStreamReaderAdapter(IEventStoreConnection eventStoreConnection) { _eventStoreConnection = eventStoreConnection; } public async Task<MyStreamEventsSlice> ReadForwardAsync( string streamId, long startPosition, int eventCount, bool resolveLinks, UserCredentials credentials = null) { var originalSlice = await _eventStoreConnection.ReadStreamEventsForwardAsync( streamId, startPosition, eventCount, resolveLinks, credentials); // 把EventStore的类型映射到自定义DTO return new MyStreamEventsSlice { StreamId = originalSlice.Stream, IsEndOfStream = originalSlice.IsEndOfStream, Events = originalSlice.Events.Select(e => new MyResolvedEvent { // 映射具体属性 }).ToList() }; } }
步骤3:测试时模拟自定义接口
// 直接模拟自己的接口,完全不用管EventStore的封闭类型 var readerMock = new Mock<IEventStoreStreamReader>(); readerMock.Setup(r => r.ReadForwardAsync( It.IsAny<string>(), It.IsAny<long>(), It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<UserCredentials>())) .ReturnsAsync(new MyStreamEventsSlice { StreamId = "test-stream", IsEndOfStream = true }); // 把这个mock注入到业务类中测试即可
这个方案的好处是彻底解耦了业务代码和第三方库,后续哪怕EventStore API更新,你只需要修改适配器,业务和测试代码都不用动。
方案3:简化测试(仅验证调用场景)
如果你的测试只需要验证ReadStreamEventsForwardAsync是否被正确调用,不需要返回值的具体属性,可以直接返回null或者用回调验证参数:
var connectionMock = new Mock<IEventStoreConnection>(); connectionMock.Setup(s => s.ReadStreamEventsForwardAsync( It.IsAny<string>(), It.IsAny<long>(), It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<UserCredentials>())) .ReturnsAsync((StreamEventsSlice)null) .Callback<string, long, int, bool, UserCredentials>((stream, start, count, resolve, creds) => { // 在这里验证参数是否符合预期 Assert.Equal("expected-stream-id", stream); Assert.Equal(0, start); });
内容的提问来源于stack exchange,提问作者J Lewis
相关产品推荐
相关产品推荐

