首页 > 解决方案 > 单元测试类中的依赖注入 IConfiguration

问题描述

我正在使用 ASP.NET Core 3,在对此处看到的具体实现进行单元测试时,我有一个包含 2 个依赖项 IConfiguration 和 ILogger 的帮助程序类:

public class Helper : IHelpers
{
    private IConfiguration configuration;
    private readonly ILogger<Helper> logger;
    public Helper(IConfiguration _config, ILogger<Helper> _logger)
    {
        configuration = _config;
        logger = _logger;
    }
    public Tuple<string, string> SpotifyClientInformation()
    {
        Tuple<string, string> tuple = null;
        try
        {
            if (configuration != null)
            {
                string clientID = configuration["SpotifySecrets:clientID"];
                //Todo move secretID to more secure location
                string secretID = configuration["SpotifySecrets:secretID"];
                tuple = new Tuple<string, string>(clientID, secretID);
            }
        }
        catch (Exception ex)
        {
            logger.LogCritical("Configuration DI not set up correctly",ex);
        }

        return tuple;
    }
}

在我的测试中,我尝试通过以下代码使用 DI 助手:

public class HelperTests
{
    private ServiceCollection serviceCollection;
    private IHelpers helper;
    [SetUp]
    public void Setup()
    {
        serviceCollection = new ServiceCollection();
        serviceCollection.AddTransient<IHelpers, Helper>();


        ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();

        helper = serviceProvider.GetService<IHelpers>();
    }

    [Test]
    public void GetSpotifyConfiguration()
    {
        Tuple<string, string> devData = helper.SpotifyClientInformation();
        Assert.NotNull(devData.Item1);
        Assert.NotNull(devData.Item2);
    }
}

但我收到错误消息:

System.InvalidOperationException : Unable to resolve service for type 
'Microsoft.Extensions.Configuration.IConfiguration' while attempting to activate 'Spotify_Angular.Helper'.

标签: c#asp.net-core

解决方案


由于您尚未在设置中使用 DI 容器注册配置或记录器接口,因此您遇到了异常。你可以这样做,但我可以建议一个替代方案。

与其创建具体的实现并注册它们/创建服务提供者,不如模拟依赖项。它将使这个过程变得更加容易,并且在你应该如何编写单元测试方面更加规范。

包括对 Moq 的引用,然后对于成功案例执行以下操作:

[Test]
public void SpotifyClientInformation_ReturnsExpectedClientIdAndSecretIdTuple()
{
    var logger = Mock.Of<ILogger<Helper>>();

    var expectedClientId = "My client Id";
    var expectedSecretId = "My secret Id";
    var configurationMock = new Mock<IConfiguration>();
    configurationMock.Setup(m => m["SpotifySecrets:clientID"]).Returns(expectedClientId);
    configurationMock.Setup(m => m["SpotifySecrets:secretID"]).Returns(expectedSecretId);
    var configuration = configurationMock.Object;

    var helper = new Helper(configuration, logger);

    var (actualClientId, actualSecretId) = helper.SpotifyClientInformation();

    Assert.Multiple(() =>
    {
        Assert.That(actualClientId, Is.EqualTo(expectedClientId));
        Assert.That(actualSecretId, Is.EqualTo(expectedSecretId));
    });
}

安排依赖关系并设置测试状态;通过调用 SpotifyClientInformation 来执行最后断言结果元组具有预期值。

从那里开始,您可以轻松地增加对空配置案例的覆盖范围

[Test]
public void SpotifyClientInformation_WithNullConfiguration_ReturnsNull()
{
    var logger = Mock.Of<ILogger<Helper>>();
    var helper = new Helper(null, logger);

    var actualResult = helper.SpotifyClientInformation();

    Assert.That(actualResult, Is.Null);
}

可以通过另一个测试来断言日志消息,但我不认为这是现实的。您所做的只是从配置实例中读取值并创建元组,我认为两者都不会产生异常。尝试读取不存在的配置键会返回 null。

如果您想使用日志消息捕获该场景,更好的方法是在构造函数中执行此操作

public class Helper : IHelpers
{
    private IConfiguration configuration;
    private readonly ILogger<Helper> logger;
    public Helper(IConfiguration _config, ILogger<Helper> _logger)
    {
        configuration = _config;
        logger = _logger;

        if (configuration == null)
        {
            logger.LogCritical("Configuration DI not set up correctly");
        }
    }
    public Tuple<string, string> SpotifyClientInformation()
    {
        if (configuration == null)
        {
            return null;
        }

        string clientID = configuration["SpotifySecrets:clientID"];
        //Todo move secretID to more secure location
        string secretID = configuration["SpotifySecrets:secretID"];
        return new Tuple<string, string>(clientID, secretID);
    }
}

然后如果你想断言日志消息

[Test]
public void Initialize_NullConfiguration_GeneratesCriticalLog()
{
    var expectedLogMessage = "Configuration DI not set up correctly";

    var logger = Mock.Of<ILogger<Helper>>();

    var helper = new Helper(null, logger);

    Mock.Get(logger).Verify(m => m.Log(
            LogLevel.Critical,
            It.IsAny<EventId>(),
            It.Is<It.IsAnyType>((o, t) => HasLogMessage(o, expectedLogMessage)),
            It.IsAny<KeyNotFoundException>(),
            (Func<It.IsAnyType, Exception, string>) It.IsAny<object>()),
        Times.Once
    );
}

private static bool HasLogMessage(object state, string expectedMessage)
{
    var loggedValues = (IReadOnlyList<KeyValuePair<string, object>>) state;
    var unformattedMessage = loggedValues[^1].Value.ToString();
    return unformattedMessage.Equals(expectedMessage, StringComparison.CurrentCultureIgnoreCase);
}

就我个人而言,我不担心像记录器和配置这样的空值保护 DI 主食。如果必须的话,我会提出一个 ArgumentNullException 以真正将其归为该类不能/不应该在没有它的情况下运行,并且我应该在部署之前修复 DI 配置。这样做意味着您可以取消SpotifyClientInformation方法中的空检查以及对消耗块的任何空检查SpotifyClientInformation


推荐阅读