You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为测试模拟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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 11:12:12