0

I am using ASP.NET Core 3, I have a helpers class that contains 2 dependencies, IConfiguration and ILogger, when unit testing the concrete implementation seen here:

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;
    }
}

In my tests, I try to DI helpers via this code:

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);
    }
}

but I get the error that reads:

System.InvalidOperationException : Unable to resolve service for type 
'Microsoft.Extensions.Configuration.IConfiguration' while attempting to activate 'Spotify_Angular.Helper'.
8
  • Does this answer your question? ASP.Net Core MVC Dependency Injection not working Commented Jan 14, 2020 at 21:09
  • @devNull unfortunately not stackoverflow.com/a/47279586/12526676 Commented Jan 14, 2020 at 21:17
  • Where do you expect the value for e.g. SpotifySecrets:clientID to come from with this setup? Commented Jan 14, 2020 at 21:22
  • I think it's less about the IConfiguration vs IConfigurationRoot and instead about registering the configuration. But I suppose the answer in that question was more about the former. Here's one that is more related to your problem: stackoverflow.com/a/46032491/5803406 Commented Jan 14, 2020 at 21:24
  • appsettings.json @KirkLarkin Commented Jan 14, 2020 at 21:25

1 Answer 1

1

You're getting an exception as you have not registered the configuration or logger interfaces with the DI container in your set up. You could do that, but may I suggest an alternative.

Rather than creating concrete implementations and registering them/creating a service provider, mock the dependencies. It'll make this process a lot easier and is more canonical in terms of how you should write your unit tests.

Include a reference to Moq then for the success case do something like:

[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));
    });
}

Arrange the dependencies and set up the test state; Act by invoking SpotifyClientInformation; and finally Assert that the resulting tuple has the expected values.

Following on from there you can easily increase your coverage for the null configuration case

[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);
}

It is possible to assert the log message with another test but I don't believe it's realistic. All you're doing is reading values from the configuration instance and creating the tuple and I don't think either will produce an exception. Attempting to read a configuration key that doesn't exist returns null.

A better way if you wanted to capture that scenario with a log message would be to do it in the constructor

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);
    }
}

Then if you want to assert the log message

[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);
}

Personally I don't worry about null guarding DI staples like loggers and configuration. If I had to, I'd raise an ArgumentNullException to really ram it home that this class can't/shouldn't operate without it and I should fix the DI configuration before deploying. Doing that would mean you could do away with the null check in the SpotifyClientInformation method plus any null checking for blocks that consume SpotifyClientInformation.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.