首页 > 解决方案 > 使用 Moq 验证属性多次更改

问题描述

在以下示例中,如何验证调用该Start()方法导致Status值更改为Startingthen 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()
    {
        // ...
    }
}

标签: c#moq

解决方案


此代码的当前设计存在与实现问题的紧密耦合,使其无法单独测试。通过手动创建要启动的服务,被测对象似乎也违反了单一职责原则 (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;
    }
}

推荐阅读