如何在C#测试中使用Moq动态修改IOptions<T>的值?
问题场景
我有如下结构的C#类,使用IOptions<MyConfigClass>映射配置文件:
public class MyDummyService { private readonly MyConfigClass _config; public MyDummyService(IOptions<MyConfigClass> options) { _config = options.Value; } public string DoSomethingWithTheNumber() { if (_config.SomeValue % 2 == 0) return "foo"; return "bar"; } } public class MyConfigClass { public int SomeValue { get; set; } public string SomeName { get; set; } }
我当前的测试代码如下(注:我知道应该用[TestCase],这里只是示例写法):
class MyDummyServiceTests { protected AutoFixture.Fixture _fixture; protected MyDummyService _sut; protected MyConfigClass _simpleConfig; protected Mock<IOptions<MyConfigClass>> _mockConfig; public MyDummyServiceTests() { _fixture = new AutoFixture.Fixture(); _mockConfig = new Mock<IOptions<MyConfigClass>>(); _simpleConfig = _fixture.Build<MyConfigClass>() .With(i => i.SomeValue, 4) .With(i => i.SomeName, "Pippo") .Create(); } [SetUp] public void Setup() { _mockConfig.SetupGet(m => m.Value).Returns(_simpleConfig); _sut = new MyDummyService(_mockConfig.Object); } [TearDown] public void TearDown() { _mockConfig.Reset(); } public class DoSomethingWithTheNumber : MyDummyServiceTests { [Test] public void Should_ReturnFoo_WhenNumberIsEven() { _simpleConfig = _fixture.Build<MyConfigClass>() .With(_ => _.SomeValue, 10) .Create(); var s = _sut.DoSomethingWithTheNumber(); Assert.AreEqual("foo", s); } [Test] public void Should_ReturnBar_WhenNumberIsOdd() { _simpleConfig = _fixture.Build<MyConfigClass>() .With(_ => _.SomeValue, 69) .Create(); var s = _sut.DoSomethingWithTheNumber(); Assert.AreEqual("bar", s); } } }
现在的问题是:测试中修改_simpleConfig后,DoSomethingWithTheNumber()拿不到新值——因为服务初始化时就已经把配置值存到私有字段里了。我想简化测试,只设置当前测试需要的配置值,不想每次都重新初始化所有东西,该怎么改进?
注:我不需要配置在运行时变化,所以IOptions<T>、IOptionsMonitor<T>、IOptionsSnapshot<T>都可以用。
改进方案
方案一:直接修改现有配置实例的属性
既然MyConfigClass是普通POCO,直接修改已创建实例的属性即可,不需要重新Build新对象:
public class DoSomethingWithTheNumber : MyDummyServiceTests { [Test] public void Should_ReturnFoo_WhenNumberIsEven() { // 直接修改现有配置的目标属性,无需重建对象 _simpleConfig.SomeValue = 10; var s = _sut.DoSomethingWithTheNumber(); Assert.AreEqual("foo", s); } [Test] public void Should_ReturnBar_WhenNumberIsOdd() { _simpleConfig.SomeValue = 69; var s = _sut.DoSomethingWithTheNumber(); Assert.AreEqual("bar", s); } }
这种方法最简单,因为服务持有的是同一个_simpleConfig实例的引用,修改属性后服务调用时会直接读取新值。
方案二:在测试中重新配置Mock并重建SUT
如果不想修改原有实例,也可以在测试里重新设置Mock的返回值,然后重建SUT:
public class DoSomethingWithTheNumber : MyDummyServiceTests { [Test] public void Should_ReturnFoo_WhenNumberIsEven() { _simpleConfig = _fixture.Build<MyConfigClass>() .With(_ => _.SomeValue, 10) .Create(); // 重新设置Mock的返回值 _mockConfig.SetupGet(m => m.Value).Returns(_simpleConfig); // 重建SUT,让它拿到新的配置 _sut = new MyDummyService(_mockConfig.Object); var s = _sut.DoSomethingWithTheNumber(); Assert.AreEqual("foo", s); } }
方案三:使用Options.Create简化Mock操作
完全不用Moq,直接用Options.Create创建IOptions<T>实例,减少Mock的复杂度:
修改测试类的SetUp:
[SetUp] public void Setup() { // 跳过Mock,直接创建IOptions实例 _sut = new MyDummyService(Options.Create(_simpleConfig)); }
测试方法中重建SUT:
[Test] public void Should_ReturnFoo_WhenNumberIsEven() { _simpleConfig = _fixture.Build<MyConfigClass>() .With(_ => _.SomeValue, 10) .Create(); // 用新配置创建IOptions并重建SUT _sut = new MyDummyService(Options.Create(_simpleConfig)); var s = _sut.DoSomethingWithTheNumber(); Assert.AreEqual("foo", s); }
方案四:使用AutoFixture+TestCase优化(最简洁)
结合[TestCase]进一步简化测试,减少重复代码,同时避免测试间的状态污染:
class MyDummyServiceTests { private readonly AutoFixture.Fixture _fixture = new AutoFixture.Fixture(); [TestCase(10, "foo")] [TestCase(69, "bar")] public void DoSomethingWithTheNumber_ReturnsExpectedValue_WhenSomeValueIs(int someValue, string expected) { // 只设置需要的属性,其他属性由AutoFixture自动填充 var config = _fixture.Build<MyConfigClass>() .With(c => c.SomeValue, someValue) .Create(); var sut = new MyDummyService(Options.Create(config)); var result = sut.DoSomethingWithTheNumber(); Assert.AreEqual(expected, result); } }
内容的提问来源于stack exchange,提问作者Davide Bellone
相关产品推荐
相关产品推荐

