如何使用AutoFixture的AutoMock测试构造函数中的Reactive代码?解决Mock初始化的“鸡生蛋”困境
解决AutoFixture+Moq构造函数中Reactive Extensions的"鸡生蛋"Mock问题
你的问题戳中了AutoFixture+Moq组合的一个典型痛点:被测类(SUT)在构造阶段就依赖了Mock对象的成员,但AutoFixture默认会先构造SUT,再让你在测试方法里配置Mock——这就导致构造函数订阅的是Mock的默认空Observable,而不是你后来设置的那个。
下面给你两个能保持测试简洁性的解决方案,不用放弃AutoMockData特性的便利:
方案一:全局配置AutoFixture,提前绑定Subject
最优雅的方式是修改你的AutoMockDataAttribute,预先给IMyService的Mock配置一个Subject<int>作为StreamOfThings的返回值。Subject既是IObservable又是IObserver,完美适配测试时动态发送值的需求。
修改后的特性代码:
public class AutoMockDataAttribute : AutoDataAttribute { private static IFixture Create() { var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); // 预先为IMyService的Mock绑定Subject,并把Subject注册到Fixture中 fixture.Customize<Mock<IMyService>>(customization => customization.Do(mock => { var thingSubject = fixture.Create<Subject<int>>(); mock.Setup(x => x.StreamOfThings).Returns(thingSubject); fixture.Register(() => thingSubject); // 让测试能直接注入Subject })); return fixture; } public AutoMockDataAttribute() : base(Create) {} }
对应的测试代码会变得异常简洁:
[Test, AutoMockData] public void Verify_some_behavior( MyViewModel vm, Subject<int> thingSubject) { // 直接发送值,构造函数里的订阅会立刻收到 thingSubject.OnNext(100); vm.SomeClickEvent(); vm.Thing.Should().Be(100); }
这个方案的核心是把Mock的配置提前到SUT构造之前,而且所有用到AutoMockData的测试都能复用这个配置,不需要重复写模板代码。
方案二:单测试局部定制(不修改全局特性)
如果不想全局修改AutoMockDataAttribute,可以在单个测试里通过IFixture的Customize方法提前配置Mock:
[Test, AutoData] public void Verify_some_behavior(IFixture fixture) { var thingSubject = new Subject<int>(); // 先配置Mock的返回值,再Freeze和创建SUT fixture.Customize<Mock<IMyService>>(c => c.Do(mock => mock.Setup(x => x.StreamOfThings).Returns(thingSubject))); var mockService = fixture.Freeze<Mock<IMyService>>(); var vm = fixture.Create<MyViewModel>(); thingSubject.OnNext(100); vm.SomeClickEvent(); vm.Thing.Should().Be(100); }
这种方式适合只在个别测试里需要特殊配置的场景,缺点是测试代码会比方案一稍繁琐。
为什么你的原测试会失败?
再帮你复盘下原因:
- AutoFixture执行测试时,会先实例化
Mock<IMyService>并Freeze,然后创建MyViewModel——这时候MyViewModel的构造函数已经订阅了mockService.StreamOfThings的默认值(Moq对IObservable的默认返回是一个空的可观察序列,不会发送任何值)。 - 你在测试里写的
mockService.Setup(...)是在构造函数执行之后才运行的,这时候构造函数订阅的已经是之前的空序列了,新设置的Observable.Return(100)根本不会被用到。
内容的提问来源于stack exchange,提问作者void.pointer




