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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 06:45:37