如何为测试模拟IoT Hub SDK v2的IotHubServiceClient?
问题场景
我有一个依赖于C# IoT Hub SDK v2预览版IotHubServiceClient的IotHubService类,用于执行IoT Hub的各类查询操作。为提升类的可靠性,我编写了单元测试并尝试模拟依赖项,但运行测试时遇到报错:
System.NotSupportedException : Invalid setup on a non-virtual (overridable in VB) member: mock => mock.Devices
已知底层DevicesClient是密封类,无法直接模拟。
测试代码
[Fact] public async Task GetDeviceByIdAsync_ValidDeviceId_ReturnsDevice() { // Arrange string deviceId = "testDeviceId"; Device expectedDevice = new Device(deviceId); // Create a mock IotHubServiceClient Mock<IotHubServiceClient> mockIotHubServiceClient = new Mock<IotHubServiceClient>(); mockIotHubServiceClient .Setup(c => c.Devices.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())) .ReturnsAsync(expectedDevice); // Create an instance of IoTHubService using the mock client IotHubService ioTHubService = new IotHubService(mockIotHubServiceClient.Object); // Act Device result = await ioTHubService.GetDeviceByIdAsync(deviceId); // Assert Assert.Equal(expectedDevice, result); mockIotHubServiceClient.Verify(c => c.Devices.GetAsync(deviceId, default(CancellationToken)), Times.Once); }
IotHubService类代码
public class IotHubService : IIotHubService { protected readonly IotHubServiceClient _iotHubServiceClient; public IotHubService(string connectionString) { _iotHubServiceClient = new IotHubServiceClient(connectionString); } public IotHubService(IotHubServiceClient iotHubServiceClient) { _iotHubServiceClient = iotHubServiceClient; } public async Task<Device> GetDeviceByIdAsync(string deviceId) { if (string.IsNullOrEmpty(deviceId)) { throw new ArgumentNullException(nameof(deviceId)); } try { return await _iotHubServiceClient.Devices.GetAsync(deviceId); } catch (Exception) { throw; } } }
解决方案
1. 引入抽象层(推荐,符合依赖倒置原则)
直接依赖SDK的具体实现会导致测试困难,因此可以为设备操作定义抽象接口,让IotHubService依赖接口而非具体的IotHubServiceClient。
步骤1:定义抽象接口
public interface IDeviceManager { Task<Device> GetDeviceAsync(string deviceId, CancellationToken cancellationToken = default); // 可根据需求添加其他设备操作方法,如CreateDeviceAsync、DeleteDeviceAsync等 }
步骤2:实现SDK适配类
将IotHubServiceClient的设备操作包装到接口实现中:
public class IotHubDeviceManager : IDeviceManager { private readonly DevicesClient _devicesClient; public IotHubDeviceManager(IotHubServiceClient serviceClient) { _devicesClient = serviceClient.Devices; } public async Task<Device> GetDeviceAsync(string deviceId, CancellationToken cancellationToken = default) { return await _devicesClient.GetAsync(deviceId, cancellationToken); } }
步骤3:修改IotHubService依赖
让IotHubService依赖IDeviceManager,同时保留生产和测试用的构造函数:
public class IotHubService : IIotHubService { private readonly IDeviceManager _deviceManager; // 生产环境构造:创建真实的SDK适配实例 public IotHubService(string connectionString) { var serviceClient = new IotHubServiceClient(connectionString); _deviceManager = new IotHubDeviceManager(serviceClient); } // 测试环境构造:注入模拟的接口实现 public IotHubService(IDeviceManager deviceManager) { _deviceManager = deviceManager; } public async Task<Device> GetDeviceByIdAsync(string deviceId) { if (string.IsNullOrEmpty(deviceId)) { throw new ArgumentNullException(nameof(deviceId)); } try { return await _deviceManager.GetDeviceAsync(deviceId); } catch (Exception) { throw; } } }
修改后的测试代码
现在可以直接模拟IDeviceManager,无需依赖SDK的具体实现:
[Fact] public async Task GetDeviceByIdAsync_ValidDeviceId_ReturnsDevice() { // Arrange string deviceId = "testDeviceId"; Device expectedDevice = new Device(deviceId); var mockDeviceManager = new Mock<IDeviceManager>(); mockDeviceManager .Setup(m => m.GetDeviceAsync(deviceId, It.IsAny<CancellationToken>())) .ReturnsAsync(expectedDevice); var ioTHubService = new IotHubService(mockDeviceManager.Object); // Act Device result = await ioTHubService.GetDeviceByIdAsync(deviceId); // Assert Assert.Equal(expectedDevice, result); mockDeviceManager.Verify(m => m.GetDeviceAsync(deviceId, default(CancellationToken)), Times.Once); }
2. 使用支持非虚成员模拟的测试框架
如果不想修改现有代码结构,可以使用支持模拟非虚成员、密封类的测试框架,如FakeItEasy或JustMock。以下是使用FakeItEasy的示例:
[Fact] public async Task GetDeviceByIdAsync_ValidDeviceId_ReturnsDevice() { // Arrange string deviceId = "testDeviceId"; Device expectedDevice = new Device(deviceId); // 模拟密封的DevicesClient var fakeDevicesClient = A.Fake<DevicesClient>(); A.CallTo(() => fakeDevicesClient.GetAsync(deviceId, default)) .ReturnsAsync(expectedDevice); // 模拟IotHubServiceClient,让其返回上面的模拟实例 var fakeServiceClient = A.Fake<IotHubServiceClient>(); A.CallTo(() => fakeServiceClient.Devices).Returns(fakeDevicesClient); var ioTHubService = new IotHubService(fakeServiceClient); // Act Device result = await ioTHubService.GetDeviceByIdAsync(deviceId); // Assert Assert.Equal(expectedDevice, result); A.CallTo(() => fakeDevicesClient.GetAsync(deviceId, default)).MustHaveHappenedOnceExactly(); }
注意:这种方法依赖框架对非公共成员的访问能力,可能会因SDK版本更新而失效,稳定性不如抽象层方案。
内容的提问来源于stack exchange,提问作者ameyers-inimco
相关产品推荐
相关产品推荐

