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.
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"));
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();
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"), ..., ...));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With