C#单元测试:如何Mock依赖网络的本地Plc变量?
如何对依赖S7NETPLUS的RetrievePlcAsync方法做单元测试
核心问题:硬编码的Plc实例无法Mock
不管是最初还是更新后的代码,RetrievePlcAsync里直接new Plc(...)的写法都是问题根源——这种硬编码实例化第三方依赖的方式,会让单元测试必须依赖实际的PLC仿真环境(甚至真实网络连接),完全没法隔离外部依赖做纯逻辑测试。而且S7NETPLUS的Plc是具体类,它的ReadClassAsync也不是虚方法,常规Mock框架没法直接Mock它。
解决方案:抽象第三方依赖,通过依赖注入解耦
要Mock这个PLC依赖,得先给S7NETPLUS的Plc套一层抽象,把业务代码和第三方实现隔离开,具体步骤如下:
1. 定义PLC操作的抽象接口
创建一个只包含业务需要的方法的接口,不用管S7NETPLUS的具体实现:
public interface IPlcClient : IDisposable { Task<T> ReadClassAsync<T>(int db) where T : class, new(); }
2. 封装S7NETPLUS的Plc实现
把原有的PLC实例化和读取逻辑包装到这个接口的实现类里:
public class S7PlcClient : IPlcClient { private readonly Plc _plc; public S7PlcClient(CpuType cpuType, string ip, int port, int rack, int slot) { _plc = new Plc(cpuType, ip, port, rack, slot); } public async Task<T> ReadClassAsync<T>(int db) where T : class, new() { return await _plc.ReadClassAsync<T>(db); } public void Dispose() { _plc.Dispose(); } }
3. 修改DataRetrieval类,注入抽象依赖
因为每次调用RetrievePlcAsync需要不同的IP参数,所以注入一个工厂委托来动态创建PLC客户端,同时把新增的_orderRepository也改成构造注入(原来的代码里应该是漏了注入逻辑):
public class DataRetrieval : IDataRetrieval { private readonly ILogger<DataRetrieval> _logger; private readonly IOrderRepository _orderRepository; private readonly Func<string, IPlcClient> _plcClientFactory; // 注入所有依赖:日志、订单仓库、PLC客户端工厂 public DataRetrieval(ILogger<DataRetrieval> logger, IOrderRepository orderRepository, Func<string, IPlcClient> plcClientFactory) { _logger = logger; _orderRepository = orderRepository; _plcClientFactory = plcClientFactory; } public async Task<PlcResponse> RetrievePlcAsync(string ip, int db) { // 通过工厂创建PLC客户端,固定参数(CpuType、端口、rack、slot)可以放到工厂里配置 using var plc = _plcClientFactory(ip); var data = await plc.ReadClassAsync<PlcResult>(db); _logger.LogInformation("Retrieve data from plc: {@Data}", data); if (data.Type == 1) { var order = await _orderRepository.GetByIdAsync(data.OrderId); return order == null ? new PlcResponse(ResponseStatus.NotExists) : new PlcResponse(ResponseStatus.Exsits, order); } else { return new PlcResponse(ResponseStatus.InvalidType); } } }
注:如果需要动态调整端口、rack等参数,可以修改工厂委托的参数列表,比如
Func<string, int, int, int, IPlcClient>
4. 编写单元测试,Mock所有抽象依赖
用Moq这类Mock框架,就能完全隔离PLC和数据库依赖,只测试业务逻辑:
[TestClass] public class DataRetrievalTests { private Mock<ILogger<DataRetrieval>> _mockLogger; private Mock<IOrderRepository> _mockOrderRepo; private Mock<IPlcClient> _mockPlcClient; private DataRetrieval _sut; // System Under Test,待测试对象 [TestInitialize] public void Setup() { _mockLogger = new Mock<ILogger<DataRetrieval>>(); _mockOrderRepo = new Mock<IOrderRepository>(); _mockPlcClient = new Mock<IPlcClient>(); // 模拟PLC客户端工厂,返回我们的Mock实例 var plcFactory = new Func<string, IPlcClient>(ip => _mockPlcClient.Object); _sut = new DataRetrieval(_mockLogger.Object, _mockOrderRepo.Object, plcFactory); } [TestMethod] public async Task RetrievePlcAsync_Type1_OrderExists_ReturnsExists() { // 准备测试数据 var testPlcData = new PlcResult { Type = 1, OrderId = 123 }; var testOrder = new Order { Id = 123 }; // Mock依赖的返回值 _mockPlcClient.Setup(p => p.ReadClassAsync<PlcResult>(It.IsAny<int>())) .ReturnsAsync(testPlcData); _mockOrderRepo.Setup(r => r.GetByIdAsync(123)) .ReturnsAsync(testOrder); // 执行测试方法 var result = await _sut.RetrievePlcAsync("192.168.0.1", 1); // 验证结果 Assert.AreEqual(ResponseStatus.Exsits, result.Status); Assert.AreEqual(testOrder, result.Order); _mockLogger.Verify(l => l.LogInformation(It.IsAny<string>(), testPlcData), Times.Once); } [TestMethod] public async Task RetrievePlcAsync_Type1_OrderNotFound_ReturnsNotExists() { var testPlcData = new PlcResult { Type = 1, OrderId = 123 }; _mockPlcClient.Setup(p => p.ReadClassAsync<PlcResult>(It.IsAny<int>())) .ReturnsAsync(testPlcData); _mockOrderRepo.Setup(r => r.GetByIdAsync(123)) .ReturnsAsync((Order)null); var result = await _sut.RetrievePlcAsync("192.168.0.1", 1); Assert.AreEqual(ResponseStatus.NotExists, result.Status); } [TestMethod] public async Task RetrievePlcAsync_TypeNot1_ReturnsInvalidType() { var testPlcData = new PlcResult { Type = 2 }; _mockPlcClient.Setup(p => p.ReadClassAsync<PlcResult>(It.IsAny<int>())) .ReturnsAsync(testPlcData); var result = await _sut.RetrievePlcAsync("192.168.0.1", 1); Assert.AreEqual(ResponseStatus.InvalidType, result.Status); } }
总结
直接MockS7NETPLUS的Plc类不可行,因为它是无虚方法的具体类。通过封装抽象接口+依赖注入的方式,我们把外部依赖完全隔离,单元测试只需要验证业务逻辑的分支判断、日志输出、依赖调用是否符合预期,完全不需要依赖PLC仿真环境。
内容的提问来源于stack exchange,提问作者Paddi
相关产品推荐
相关产品推荐

