使用Moq/AutoMoq与xUnit覆盖AutoFixture ICustomization注册问题
问题场景与代码
架构说明
层级结构:Controller→Service→ClientWrapper→Client,编写集成测试时Mock最底层的ICalendarServiceClient,通过调用Controller验证Client是否被正确调用。
CalendarControllerBuilder实现
internal class CalendarControllerBuilder { public CalendarControllerBuilder() { CalendarClientMock = new Mock<ICalendarServiceClient>(MockBehavior.Strict); } public Mock<ICalendarServiceClient> CalendarClientMock { get; set; } public CalendarControllerBuilder With(Mock<ICalendarServiceClient> calendarClientWrapperMock) { CalendarClientMock = calendarClientWrapperMock; return this; } public CalendarController Create() { var calendarClientWrapperMock = new CalendarClientWrapper(CalendarClientMock.Object); var calenderService = new CalendarService(calendarClientWrapperMock); return new CalendarController(calenderService); } }
自定义ICustomization配置
internal class CalenderControllerCustomization : ICustomization { public void Customize(IFixture fixture) { fixture.Register<CalendarController>(() => { // ----- ICalendarServiceClient mock setups ----- var calendarServiceClientMock = new Mock<ICalendarServiceClient>(MockBehavior.Strict); calendarServiceClientMock.Setup(m => m.GetEvents(It.IsAny<DateTime>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>())) .ReturnsAsync(fixture.Create<EventList>()).Verifiable(); return new CalendarControllerBuilder() .With(calendarServiceClientMock) .Create(); }); } }
AutoMoq数据属性
public class Attributes { public class AutoMoqDataAttribute<T> : AutoDataAttribute where T : ICustomization, new() { public AutoMoqDataAttribute() : base(() => new Fixture() .Customize( new CompositeCustomization( new AutoMoqCustomization(), new T()))) { } } public class AutoMoqDataAttribute : AutoDataAttribute { public AutoMoqDataAttribute() : base(() => new Fixture() .Customize( new AutoMoqCustomization())) { } } }
正常运行的测试用例
[Theory] [AutoMoqData<CalenderControllerCustomization>] public async Task Test_GetAllEvents_ClientIsCalledCorrectlyAndReturnsCorrectData( IFixture fixture, [Frozen] Mock<ICalendarServiceClient> calendarServiceClientMock, CalendarController sut) { // Arrange var startDate = fixture.Create<DateTimeOffset>(); var endDate = fixture.Create<DateTimeOffset>(); // Act var eventList = await sut.GetAllEvents(startDate, endDate); // Assert eventList.Events.Count.Should().Be(3); // fixture always create 3 of lists here calendarServiceClientMock.Verify(); }
问题描述
尝试在测试中覆盖ICalendarServiceClient Mock的返回数据时,验证失败:
[Theory] [AutoMoqData<CalenderControllerCustomization>] public async Task Test_GetAllEvents_ClientIsCalledCorrectlyAndReturnsCorrectData_Overridden( IFixture fixture, [Frozen] Mock<ICalendarServiceClient> calendarServiceClientMock, CalendarController sut) { // Arrange var startDate = fixture.Create<DateTimeOffset>(); var endDate = fixture.Create<DateTimeOffset>(); var result = fixture.Build<EventList>() .With(x => x.Events, fixture.CreateMany<Event>(5).ToList()) .Create(); // override client mock setup: calendarServiceClientMock.Setup(m => m.GetEvents(It.IsAny<DateTime>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>())) .ReturnsAsync(fixture.Build<EventList>() .With(x => x.Events, fixture.CreateMany<Event>(5).ToList()) .Create()).Verifiable(); // Act var eventList = await sut.GetAllEvents(startDate, endDate); // Assert eventList.Events.Count.Should().Be(5); // FAILS - IT GETS 3 AND NOT 5 calendarServiceClientMock.Verify(); // THIS FAILS TOO IF I SET 3 IN ABOVE }
无论是否使用[Frozen]属性,问题依旧。希望避免重复调用CalendarControllerBuilder以减少样板代码,请问遗漏了什么配置?
解决方案
问题核心在于测试中拿到的calendarServiceClientMock和Controller依赖链中实际使用的Mock不是同一个实例。
原CalenderControllerCustomization里手动创建了新的Mock<ICalendarServiceClient>实例并传入构建器,但AutoFixture注入到测试方法中的Mock是AutoMoq自动生成的另一个独立实例——两者毫无关联,所以测试中覆盖的配置根本不会被Controller调用到。
修复步骤:
- 修改Customization,复用AutoFixture管理的Mock实例
internal class CalenderControllerCustomization : ICustomization { public void Customize(IFixture fixture) { // 从Fixture中获取AutoMoq创建的Mock实例(自动冻结) var calendarServiceClientMock = fixture.Create<Mock<ICalendarServiceClient>>(); calendarServiceClientMock.Setup(m => m.GetEvents(It.IsAny<DateTime>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>())) .ReturnsAsync(fixture.Create<EventList>()) .Verifiable(); // 注册Controller创建逻辑,复用同一个Mock实例 fixture.Register<CalendarController>(() => new CalendarControllerBuilder() .With(calendarServiceClientMock) .Create()); } }
- 调整测试方法,兼容Strict模式的覆盖逻辑
[Theory] [AutoMoqData<CalenderControllerCustomization>] public async Task Test_GetAllEvents_ClientIsCalledCorrectlyAndReturnsCorrectData_Overridden( IFixture fixture, Mock<ICalendarServiceClient> calendarServiceClientMock, CalendarController sut) { // Arrange var startDate = fixture.Create<DateTimeOffset>(); var endDate = fixture.Create<DateTimeOffset>(); var customEventList = fixture.Build<EventList>() .With(x => x.Events, fixture.CreateMany<Event>(5).ToList()) .Create(); // 重置原有Setup,避免Strict模式下重复配置冲突 calendarServiceClientMock.Reset(); calendarServiceClientMock.Setup(m => m.GetEvents(It.IsAny<DateTime>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>())) .ReturnsAsync(customEventList) .Verifiable(); // Act var eventList = await sut.GetAllEvents(startDate, endDate); // Assert eventList.Events.Count.Should().Be(5); calendarServiceClientMock.Verify(); }
关键说明:
- 使用
fixture.Create<Mock<ICalendarServiceClient>>()让AutoMoq统一管理Mock实例,保证整个Fixture上下文里只有这一个Mock被复用。 - 因为启用了
MockBehavior.Strict,覆盖配置前必须调用Reset()清除原有Setup,否则会因重复配置抛出异常。
内容的提问来源于stack exchange,提问作者Morten_564834
相关产品推荐
相关产品推荐

