c# - 使用 Moq 验证属性多次更改
问题描述
在以下示例中,如何验证调用该Start()
方法导致Status
值更改为Starting
then 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()
{
// ...
}
}
解决方案
此代码的当前设计存在与实现问题的紧密耦合,使其无法单独测试。通过手动创建要启动的服务,被测对象似乎也违反了单一职责原则 (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;
}
}
推荐阅读
- node.js - 如何在 MERN 堆栈应用程序中发送多个数据?
- docker - 无法使用 netowrk_mode: 主机访问 docker 容器中的 jupyterhub
- javascript - 需要使用切换更改复选框
- nuget - 执行本地 NuGet 包安装但收到错误 - 无法获取存储库签名信息
- php - Laravel if 刀片视图上的语句
- confluent-platform - Cloud Confluent kafka 安装在 ubuntu 18 中失败
- python - 使用 while 循环查找函数中的错误
- android - 动态添加的视图在 invalidate() 后不调用 onDraw
- swift - 检测自定义 UITextField 是否有文本值
- javascript - 如何监听 CytoscapeJS 节点的拖动事件(仅包括光标/手指正下方的那个)