2

I have an interface which is used in an MVC controller which gets some data. To keep it simple the interface so far looks something like this:

public interface IDataProvider
{
    DataModel GetData();
}

I have appropriate unit tests for this interface where it is called in the action. However, in the real implementation this will call a web service which of course could throw an exception, therefore if it does I want to write a test to ensure that I log a message if an error occurs.

To do this I have a logger interface which is actually an interface to NLog called ILogger. I could do this:

public interface IDataProvider
{
    DataModel GetData(ILogger logger);
}

This would allow me to run unit tests for the logger making it nice and simple. However, I don't think this is the right way of doing this because the logger is really unrelated to this method. Also, if I start adding other methods to this interface which I need logging for then I will have to include the logger in the parameter of all of those methods as well.

The best way I can think of right now is to include the logger in the constructor of my implementation which might look like this:

public class DataProvider : IDataProvider
{
    private readonly ILogger _logger;

    public DataProvider(ILogger logger)
    {
        _logger = logger;
    }

    public DataModel GetData()
    {
        // CODE GOES HERE
    }
}

However this means that I cannot test the logger in my unit tests. What is the best way to achieve this so that I can keep the logger separate from the method and make it testable?

I'll appreciate any help, thanks.

EDIT:

I realise I missed out unit testing code here is what I mean:

At the moment I am ensuring that GetData is called in my action this way:

var controller = new DataController(_dataProvider.Object);

controller.Index();

_dataProvider.Verify(dataProvider => dataProvider.GetData());

What I'd like to do is the same but for the logger but only if an exception is thrown like this:

_dataProvider.Setup(dataProvider => dataProvider.GetData()).Throws<WebException>();

var controller = new DataController(_dataProvider.Object);

controller.Index();

_logger.Verify(logger => logger.ErrorException(It.IsAny<string>(), It.IsAny<Exception>());

Obviously logger would be given to data provider in the setup. I hope that makes a bit more sense.

Serberuss
  • 2,247
  • 4
  • 22
  • 40
  • 3
    Please explain this: _However this means that I cannot test the logger in my unit tests._ – Austin Salonen Mar 27 '13 at 16:08
  • 2
    For a question talking so much about unit test, you haven't show a single piece of unit test code. – manojlds Mar 27 '13 at 16:10
  • Using a mocking framework I can ensure that the logger is called if I pass it into the method parameter. I cannot do that if I only have the logger passed into the constructor of the implementation because I am mocking the objects in my unit tests – Serberuss Mar 27 '13 at 16:11
  • I challenge that you can, in fact, do exactly that. – Austin Salonen Mar 27 '13 at 16:12
  • Brilliant, though I'm not sure how. I've updated my question, sorry for the confusion – Serberuss Mar 27 '13 at 16:16
  • You're testing too deep. When testing `DataController`, you shouldn't care what any concrete `IDataProvider` is doing, nor should it matter. When testing 'DataProvider' (ie, a concrete `IDataProvider`), you should care what is logged. If the controller must care, then an ILogger setter should be required of the `IDataProvider` interface. – Austin Salonen Mar 27 '13 at 17:07

4 Answers4

6

You can try using the factory pattern.

What happens here is in your production code, you are getting the logger from a Factory. In this factory, it returns either your real logger, or a fake logger which is setup in your unit tests. To your production code, it makes no difference what-so-ever.

In your Unit Tests, you are using a fake logger created using Moq. This fake allows you to test that an interface method was called, in this case ILogger.Log(). This is done by using the .Verify method.

Try something like this:

ILogger.cs

public interface ILogger
{
    void Log(string message);
}

LoggerFactory.cs

public static class LoggerFactory
{
    public static ILogger Logger
    {
        get
        {
            return LoggerFactory._logger == null ? new Logger() : LoggerFactory._logger;
        }
        set
        {
            LoggerFactory._logger = value;
        }
    }
    private static ILogger _logger = null;
}

DataProvider.cs

public void GetData()
{
    var logger = LoggerFactory.Logger;

    logger.Log("..."); // etc...
}

UnitTest.cs

private void Mock<ILogger> _mockLogger = null;

public void Load()
{
    this._mockLogger = new Mock<ILogger>();

    LoggerFactory.Logger = _mockLogger.Object;
}

public void UnitTest()
{
    // test as required

    this._mockLogger.Verify(m => m.Log(It.IsAny<string>()));
}
rhughes
  • 9,257
  • 11
  • 59
  • 87
  • +1 - possible approach, also using code in OP's sample with passing dependency in constructor give much less chances to impact other tests by leaking logger from this test (i.e. other test forgets to set logger and start failing if order of execution is different). – Alexei Levenkov Mar 27 '13 at 16:21
  • Consider adding link to mocking framework (Moq?) you are showing. – Alexei Levenkov Mar 27 '13 at 16:22
1

If you need to test that the logger is being called, I suggest using a test double called a "spy". This would not do any logging, but keep track of which methods (if any) were called. Then you can verify that the logger is called in specific instances.

You could do this by using a mocking framework to create the double (or mock) for you. Or you could create the ILogger implementation yourself. For example:

class LoggerSpy : ILogger
{
    public string LogWasCalled;

    public void Log(string message)
    {
        LogWasCalled = true;;
    }
}

The following seems to have an example of mocking an ILogger using Moq: How to Mock ILogger / ILoggerService using Moq

Community
  • 1
  • 1
Peter Ritchie
  • 35,463
  • 9
  • 80
  • 98
1

Use a mocking framework (e.g. Moq or RhinoMocks) to verify that the logger was called. Then the final code block you post, where the logger is passed in via the constructor, will work.

Andy Nichols
  • 2,952
  • 2
  • 20
  • 36
1

Passing logger (or any other dependencies) in constructor is very standard practice and allows you to use dependency injection framework if needed.

I'm not sure why you see passing logger in constructor as limiting for unit test: you have 3 components that you can test separately

  • controller (depends on data provide, mock this dependency to test),
  • data provider (depends on logging and some other classes that let you call web service - mock all dependencies so you know when logging called and no need to call Web service)
  • logging - not sure what it depends on, but should be testable separately.

Notes:

  • use mocking framework (i.e. moq ) for your tests - you'll be able to provide any implementations of interfaces (including exceptions) very easily.
  • see if dependency injection framework (i.e. Unity ) would work for you. MVC4 is very well suited for it.
Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
  • I might be doing this the wrong way but DataProvider calls a web service so that class isn't tested the same way the controller is. Instead of having the web service call in that DataProvider should another interface call be used there instead, effectively adding another layer and then include an integration test? – Serberuss Mar 27 '13 at 16:30
  • @Serberuss, I think that DataProvider does 2 things - calls web service and transforms resulting data - in this case it makes sense to split and test separately. So yes consider another interface or some other way to remove the need to call web service for unit tests. You'll face the same "web service failed with exception" issue when testing DataProvider anyway :) . Eventually lowest level will be simple wrapper on some framework method(s) that you will test with integration tests. But usually interesting problems are in parsing/data manipulation, not in getting data... – Alexei Levenkov Mar 27 '13 at 16:46