Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How I can I do TDD with Caller Info attributes?

In C# 5, they introduced the Caller Info attributes. One of the useful applications is, quite obviously, logging. In fact, their example given is exactly that:

public void TraceMessage(string message,
        [CallerMemberName] string memberName = "",
        [CallerFilePath] string sourceFilePath = "",
        [CallerLineNumber] int sourceLineNumber = 0)
{
    Trace.WriteLine("message: " + message);
    Trace.WriteLine("member name: " + memberName);
    Trace.WriteLine("source file path: " + sourceFilePath);
    Trace.WriteLine("source line number: " + sourceLineNumber);
}

I am currently developing an application and I am at the point where I would like to introduce the usage of the caller info in my logging routines. Assume that I had the relatively simple logging interface of:

public interface ILogger
{
    void Info(String message);
}

Normally, I'd use Moq to verify the behavior I desire:

// Arrange
var logger = new Mock<ILogger>();
var sut = new SystemUnderTest(logger.Object);

// Act
sut.DoIt();

// Assert
logger.Verify(log => log.Info("DoIt was called"));

That's all fine. Now I'd like to modify my logging interface such that it accepts caller info parameters:

public interface ILogger
{
    void Info(String message, [CallerMemberName] string memberName = "",
                              [CallerFilePath] string sourceFilePath = "",
                              [CallerLineNumber] int sourceLineNumber = 0);
}

For brevity, you can assume the implementation is similar to the TraceMessage example above. I can't simply create and verify my mock as above. The compiler error I receive is:

An expression tree may not contain a call or invocation that uses optional arguments

The only way around this is to use the It.IsAny<T> matcher in Moq:

// Arrange
var logger = new Mock<ILogger>();
var sut = new SystemUnderTest(logger.Object);

// Act
sut.DoIt();

// Assert
logger.Verify(log => log.Info("DoIt was called",
              It.IsAny<string>(), It.IsAny<String>(), It.IsAny<int>()));

Unfortunately, I can't assert or verify that the call site looks the way I expect it to:

public void DoIt()
{
    // do hard work
    _logger.Info("DoIt was called");
}

Which brings me to my question: How can I verify the behavior of Caller Info attributes in a unit test?

I don't particularly like the It.IsAny<T> hack. I can write the unit test, run through a red-green cycle and all is well until someone tries to modify it. Then, someone can come along and modify my implementation to contain erroneous parameters and the test will still pass.


Solution

Based on Herr Kater's answer, I was able to wrap the caller information into a utility class with the following method:

public CallerInfo GetCallerInformation()
{
    var frame = new StackFrame(2, true);

    return new CallerInfo
    {
        FileName = frame.GetFileName(),
        MethodName = frame.GetMethod().Name,
        LineNumber = frame.GetFileLineNumber()
    };
}

I could then inject this dependency into my code and verify that the logger implementation was properly using it. My callers can now be properly tested if they are using the logging correctly.

// Arrange
var backingLog = new IMock<IBackingLog>();

var callerInfoUtility = new Mock<ICallerInfoUtility>();

var info = new CallerInfo { MethodName = "Test", FileName = "File", LineNumber = 123 };
callerInfoUtility.Setup(utility => utility.GetCallerInformation()).Returns(info);

var logger = new Logger(backingLog.Object, callerInfoUtility.Object);

// Act
logger.Log("test");

// Assert
logger.Verify(log => log.Info("test was called: Line 123 of Test in File"));
like image 676
Mike Bailey Avatar asked Nov 02 '22 15:11

Mike Bailey


2 Answers

You can get some information from the StackFrame object.

 var stackFrame = new System.Diagnostics.StackFrame(1, true);
 var fileName = stackFrame.GetFileName();
 var lineNumber = stackFrame.GetFileLineNumber();
 var callerMethod = stackFrame.GetMethod();
like image 135
Herr Kater Avatar answered Nov 15 '22 06:11

Herr Kater


Instead of It.IsAny<string>() you could use It.Is<string>(callerMemberName => callerMemberName == "ExpectedCallerMemberName") to verify the the functionality of this parameter, like this:

logger.Verify(log => log.Info("DoIt was called",
    It.Is<string>(callerMemberName => callerMemberName == "ExpectedCallerMemberName"), ..., ...));
like image 22
Loren Paulsen Avatar answered Nov 15 '22 06:11

Loren Paulsen