使用 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;
}
}
在下面的示例中,我如何验证调用 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;
}
}