使用 Moq 验证多次更改的 属性

Using Moq to verify a property changing multiple times

在下面的示例中,我如何验证调用 Start() 方法导致 Status 值更改为 Starting 然后 Running

public class ServiceSettings 
{

}
public enum ServiceStatus 
{
    Stopped, 
    Stopping, 
    Starting, 
    Running
} 

public class SomeServiceHost
{
    public ServiceStatus Status => _serviceStatus;

    private ServiceStatus _serviceStatus = ServiceStatus.Stopped;
    private List<SomeActualService> _services;

    public SomeServiceHost(List<ServiceSettings> serviceSettings)
    {
        foreach(var settings in serviceSettings)
        {
            _services.Add(new SomeActualService(settings));
        }
    }

    public void Start()
    {
        _serviceStatus = ServiceStatus.Starting;

        foreach(SomeActualService service in _services)
        {
            service.Start();
        }

        _serviceStatus = ServiceStatus.Running;
    } 
}

public class SomeActualService 
{
    // I believe the context of this service class is irrelevant, as it's not accessible from the SomeServiceHost
    public SomeActualService(ServiceSettings settings)
    {
        // ...
    }

    public void Start()
    {
        // ...
    }
}

如果 Status 是一个常规 属性 这样 public ServiceStatus Status { get; set; } 那么你可以使用 VerifySet:

_someService.VerifySet(s => s.Status == ServiceStatus.Starting, Times.Once);
_someService.VerifySet(s => s.Status == ServiceStatus.Running, Times.Once);

此代码的当前设计与实现问题紧密耦合,使其无法单独测试。通过手动创建要启动的服务,被测对象似乎也违反了单一职责原则 (SRP) 和关注点分离 (SoC)。

我的建议是尽可能重构被测对象

在实现主要目标之前的一些设置和参与的成员。

服务抽象与实现

public interface IService {
    void Start();
}

public class SomeActualService : IService {

    public SomeActualService(ServiceSettings settings) {
        // ...
    }

    public void Start() {
        // ...
    }
}

服务存储库抽象和实现

public interface IServiceRepository {
    IEnumerable<IService> Get();
}

public class ServiceRepository : IServiceRepository {
    private readonly List<IService> services = new List<IService>();

    public ServiceFactory(List<ServiceSettings> serviceSettings) {
        foreach (var settings in serviceSettings) {
            services.Add(new SomeActualService(settings));
        }
    }

    public IEnumerable<IService> Get() {
       return services;
    }
}

重构测试对象

public class SomeServiceHost {
    private readonly List<IService> services = new List<IService>();

    public SomeServiceHost(IServiceRepository repository) {
        services = repository.Get().ToList();
    }

    public ServiceStatus Status { get; private set; } = ServiceStatus.Stopped;

    public void Start() {
        Status = ServiceStatus.Starting;

        foreach (var service in services) {
            service.Start();
        }

        Status = ServiceStatus.Running;
    }
}

这些抽象现在允许对被测对象进行隔离单元测试,而不会出现任何不良行为,因为它现在已与实现细节分离。

所有的实现也可以单独单独测试。使代码更加灵活和可维护。

例如,以下测试通过启动过程验证预期的状态变化。

[TestClass]
public class SomeServiceHostTests {
    [TestMethod]
    public void Should_Start_Services() {
        //Arrange
        var service = new Mock<IService>();

        var repository = Mock.Of<IServiceRepository>(_ => _.Get() == new[] { service.Object });

        var subject = new SomeServiceHost(repository);

        ServiceStatus before = subject.Status;
        ServiceStatus during = default(ServiceStatus);
        service.Setup(_ => _.Start()).Callback(() => during = subject.Status);

        //Act
        subject.Start();
        ServiceStatus after = subject.Status;

        //Assert
        before.Should().Be(ServiceStatus.Stopped);
        during.Should().Be(ServiceStatus.Starting);
        after.Should().Be(ServiceStatus.Running);

        service.Verify(_ => _.Start());//invoked at least once;
    }
}