-
-
Notifications
You must be signed in to change notification settings - Fork 805
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cannot verify calls to ILogger in .NET Core 3.0 Preview 8 (Generic Type Matcher doesn't work with Verify) #918
Comments
@hannahchan: Thanks for reporting! TL;DR: Not yet quite sure why it works in .NET Core 2.2, but with 3.0 Preview 8 it is due to the last parameter
The important bit (for now) is that The new generic type argument matcher support in Moq is new, and one thing I decided to leave out for the first version (for performance reasons) is support for such "composite" / "nested" types. See #908 (comment), where I wrote:
(Sorry for not making that clearer in the changelog or quickstart. I was quite excited to finally be able to get the feature out there so that people can start using it. But I suppose we could spend some more time on documentation. 😉) |
Ah cool, thanks for your quick response. I'm very excited about this feature too. I can confirm that your suggested workaround works and that I'm happy with it. I guess you can close this issue now. |
Can it be used also with the It.Is ? with .NET Core 3 and Moq 4.13.0
does not seem to work, I get the following: It is impossible to call the provided strongly-typed predicate due to the use of a type matcher. Provide a weakly-typed predicate with two parameters (object, Type) instead. (Parameter 'match') |
Think about it: Do what the error message tells you to do and rewrite the -It.Is<It.IsAnyType>(v => v.ToString().Contains(message))
+It.Is<It.IsAnyType>((object v, Type _) => v.ToString().Contains(message)) but it might be simpler in this case to just write |
Thanks for that, just doing |
A bit of information that might be related to why this worked in ASP.NET Core 2.2 and not in ASP.NET Core 3.0: |
Does not seem to be working: Getting "Expected invocation on the mock at least once, but was never performed: l => l.Log(LogLevel.Error, It.IsAny(), It.Is<It.IsAnyType>((v, _) => True), null, It.IsAny<Func<object, Exception, string>>()))" |
The problem is with the "Func<>". Thats works
|
I'm trying to setup a callback and it's not working. I tried with different configurations:
Does anyone know what I'm doing wrong or if it's possible to do this at all? |
@leosvelperez, see above: #918 (comment). |
@stakx thanks for pointing to that comment, I missed that. However, I did try the workaround suggested in there and I still can't get it to work. I tried a couple of variations in the Callback signature:
Edit: I tried using the
I'm still not able to use the |
@leosvelperez - I see. You appear to be assuming that Moq lets you use an For now, you could specify a Again, I am planning to make things a little easier for everyone by allowing |
I'm going to close this issue since this problem has been solved. I'm aware that the new type matcher feature is still a little rough around the edges; however, one Q&A-style issue like this one cannot replace proper documentation (which is sorely needed). Do open new issues if you keep having problems with type matchers. We'll eventually get there. |
Has anyone figured out the CallBack magic syntax yet? I've tried everything leosvelperez tried. Plus all the interfaces in the definition of FormattedLogValues . Aka, all the thing below. internal readonly struct FormattedLogValues : IReadOnlyList<KeyValuePair<string, object>>, IEnumerable<KeyValuePair<string, object>>, IEnumerable, IReadOnlyCollection<KeyValuePair<string, object>> Has anyone gotten .Callback to work yet? Looping over the IInvocation invocation .Arguments ........seems wrong in today's world (early 2020 at the time of writing this) |
If anyone could help, it would be greatly appreciated. Something else I tried below (uncommented code)... to hopefully avoid looking and casting the invocation.Arguments (commented out code).
|
…NET Core 3.1 migrate Fabrikam.DroneDelivery.DeliveryService to ASP.NET Core 3.1 - migrate Fabrikam.DroneDelivery.Common to .NET Core 3.1 - migrate Fabrikam.DroneDelivery.DeliveryService to .NET Core 3.1 - remove view support - migrate Fabrikam.DroneDelivery.DeliveryService.Tests to .NET Core 3.1 - migrate TestServer to .NET Core 3.1 - change assert from NotFound to MethodNotAllowed. For more information: dotnet/aspnetcore#11260 (comment) - fix unsupported deserialization in the new System.Text.Json. For more information: https://github.com/dotnet/corefx/issues/38569#issuecomment-502957651 - fix global logger middleware tests. For more information: devlooped/moq#918 solved: #160880
…NET Core 3.1 migrate Fabrikam.DroneDelivery.DeliveryService to ASP.NET Core 3.1 - migrate Fabrikam.DroneDelivery.Common to .NET Core 3.1 - migrate Fabrikam.DroneDelivery.DeliveryService to .NET Core 3.1 - remove view support - migrate Fabrikam.DroneDelivery.DeliveryService.Tests to .NET Core 3.1 - migrate TestServer to .NET Core 3.1 - change assert from NotFound to MethodNotAllowed. For more information: dotnet/aspnetcore#11260 (comment) - fix unsupported deserialization in the new System.Text.Json. For more information: https://github.com/dotnet/corefx/issues/38569#issuecomment-502957651 - fix global logger middleware tests. For more information: devlooped/moq#918 solved: #160880
@granadacoder You will have to cast but var logger = Mock.Of<ILogger<MyService>>();
string? logMessage = null;
Mock
.Get(logger)
.Setup(x => x.Log(
LogLevel.Information,
0,
It.IsAny<object>(),
null,
(Func<object, Exception, string>)It.IsAny<object>()))
.Callback<IInvocation>(invocation =>
{
_ = (LogLevel)invocation.Arguments[0]; // The first two will always be whatever is specified in the setup above
_ = (EventId)invocation.Arguments[1]; // so I'm not sure you would ever want to actually use them
var state = invocation.Arguments[2];
var exception = (Exception?)invocation.Arguments[3];
var formatter = invocation.Arguments[4];
var invokeMethod = formatter
.GetType()
.GetMethod("Invoke");
logMessage = (string?)invokeMethod?.Invoke(formatter, new[] { state, exception });
})
.Verifiable(); |
Here's my attempt of some functions that enable logged object and logged message and logged exception verification by type. You can extract whichever function serves your needs but the core one is public static class LoggerMockExtensions
{
public static void VerifyLog<TCategoryName, TException>(
this Mock<ILogger<TCategoryName>> loggerMock,
LogLevel expectedLogLevel,
Func<object?, bool>? stateMatcher = null,
Expression<Func<TException?, bool>>? exceptionMatcher = null)
where TException : Exception
{
exceptionMatcher ??= actualException => true;
Expression<Func<Exception?, bool>> matcher = exception =>
exception is TException && exceptionMatcher.Compile()(exception as TException);
loggerMock.VerifyLog(expectedLogLevel, stateMatcher, matcher);
}
public static void VerifyLog<TCategoryName>(
this Mock<ILogger<TCategoryName>> loggerMock,
LogLevel expectedLogLevel,
Func<object?, bool>? stateMatcher = null,
Expression<Func<Exception?, bool>>? exceptionMatcher = null)
{
stateMatcher ??= actualMessage => true;
exceptionMatcher ??= actualException => true;
loggerMock.Verify(logger =>
logger.Log(
expectedLogLevel,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((state, type) => VerifyState(state, stateMatcher)),
#pragma warning disable CS8620
// Argument cannot be used for parameter due to differences in the nullability of reference types.
It.Is<Exception>(exceptionMatcher),
#pragma warning restore CS8620
//It.Is<Func<It.IsAnyType, Exception, string>>(formatter => VerifyFormatter(formatter))),
It.Is<Func<It.IsAnyType, Exception, string>>((obj, type) => true)),
Times.Once);
}
public static void VerifyLog<TCategoryName>(
this Mock<ILogger<TCategoryName>> loggerMock,
LogLevel expectedLogLevel, string expectedMessage) =>
loggerMock.VerifyLog(
expectedLogLevel,
expectedMessage,
exceptionMatcher: null);
public static void VerifyLog<TCategoryName>(
this Mock<ILogger<TCategoryName>> loggerMock,
LogLevel expectedLogLevel, string expectedMessage,
Expression<Func<Exception?, bool>>? exceptionMatcher) =>
loggerMock.VerifyLog(
expectedLogLevel,
stateMatcher: actualMessage => actualMessage is string str && str == expectedMessage,
exceptionMatcher);
static bool VerifyState(
object? value, Func<object?, bool> userStateMatcher)
{
if (value != null && value.GetType() == FormattedLogValuesType)
value = value.ToString();
return userStateMatcher(value);
}
static readonly Type FormattedLogValuesType =
typeof(ILogger).Assembly.GetType("Microsoft.Extensions.Logging.FormattedLogValues")!;
} Example usage: public class LoggerMockExtensionsTests
{
[Fact]
public void Should_verify_log_message_passes()
{
// arrange
var message = "Information message.";
var loggerMock = new Mock<ILogger<int>>();
var logger = loggerMock.Object;
logger.LogInformation(message);
// act & assert
loggerMock.VerifyLog(LogLevel.Information, message);
}
[Fact]
public void Should_verify_log_message_fails()
{
// arrange
var actualMessage = "Information message.";
var differentMessage = "Different message.";
var loggerMock = new Mock<ILogger<int>>();
var logger = loggerMock.Object;
logger.LogInformation(actualMessage);
// act & assert
Assert.ThrowsAny<MockException>(() =>
loggerMock.VerifyLog(LogLevel.Information, differentMessage));
}
[Fact]
public void Should_verify_logged_exception_passes()
{
// arrange
var loggerMock = new Mock<ILogger<int>>();
var expectedParamName = "someParamName";
var exception = new ArgumentOutOfRangeException(expectedParamName);
var logger = loggerMock.Object;
logger.LogError(exception, exception.Message);
// act & assert
loggerMock.VerifyLog<int, ArgumentOutOfRangeException>(
LogLevel.Error,
stateMatcher: state => state as string == exception.Message,
exceptionMatcher: exception => exception != null && exception.ParamName == expectedParamName);
}
[Fact]
public void Should_verify_logged_exception_fails()
{
// arrange
var loggerMock = new Mock<ILogger<int>>();
var expectedParamName = "someParamName";
var differentParamName = "anotherParamName";
var exception = new ArgumentOutOfRangeException(differentParamName);
var logger = loggerMock.Object;
logger.LogError(exception, exception.Message);
// act
Assert.Throws<MockException>(() =>
loggerMock.VerifyLog<int, ArgumentOutOfRangeException>(
LogLevel.Error,
stateMatcher: state => state as string == exception.Message,
exceptionMatcher: exception => exception != null && exception.ParamName == expectedParamName));
}
} |
I have created a package to deal with verifying the ILogger calls just like you would expect to do it using Moq. With Moq you pass an expression and use the Moq constructs like This problem is solved only if we translate the expressions like Basically, with this package you can write tests as you usually do with Moq.
|
@adrianiftode it works great, thank you! |
A brief update, I've just merged support for "nested" type matchers in #1092. The upcoming version (4.15.0) should natively support the code example initially mentioned:
The workaround originally suggested above – |
This new version includes support for "nested" type matchers, allowing us to get rid of some workarounds originally suggested in devlooped/moq#918
Verified this works in the latest Moq and 4.8 .NET Framework via an extension method to clean things up a bit: public static void VerifyAtLeastOnce<T>(
this Mock<ILogger<T>> logger, LogLevel level) =>
logger.Verify(m => m.Log(
level,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()),
Times.AtLeastOnce); |
Bumps [Moq](https://github.com/moq/moq4) from 4.14.7 to 4.15.2. #Changelog *Sourced from [Moq's changelog](https://github.com/moq/moq4/blob/master/CHANGELOG.md).* > ## 4.15.2 (2020-11-26) > > #### Changed > > * Upgraded `System.Threading.Tasks.Extensions` dependency to version 4.5.4 (@JeffAshton, [#1108](devlooped/moq#1108)) > > > ## 4.15.1 (2020-11-10) > > #### Added > > * New method overloads for `It.Is`, `It.IsIn`, and `It.IsNotIn` that compare values using a custom `IEqualityComparer<T>` (@weitzhandler, [#1064](devlooped/moq#1064)) > * New properties `ReturnValue` and `Exception` on `IInvocation` to query recorded invocations return values or exceptions (@MaStr11, [#921](devlooped/moq#921), [#1077](devlooped/moq#1077)) > * Support for "nested" type matchers, i.e. type matchers that appear as part of a composite type (such as `It.IsAnyType[]` or `Func<It.IsAnyType, bool>`). Argument match expressions like `It.IsAny<Func<It.IsAnyType, bool>>()` should now work as expected, whereas they previously didn't. In this particular example, you should no longer need a workaround like `(Func<It.IsAnyType, bool>)It.IsAny<object>()` as originally suggested in [#918](devlooped/moq#918). (@stakx, [#1092](devlooped/moq#1092)) > > #### Changed > > * Event accessor calls (`+=` and `-=`) now get consistently recorded in `Mock.Invocations`. This previously wasn't the case for backwards compatibility with `VerifyNoOtherCalls` (which got implemented before it was possible to check them using `Verify{Add,Remove}`). You now need to explicitly verify expected calls to event accessors prior to `VerifyNoOtherCalls`. Verification of `+=` and `-=` now works regardless of whether or not you set those up (which makes it consistent with how verification usually works). (@80O, @stakx, [#1058](devlooped/moq#1058), [#1084](devlooped/moq#1084)) > * Portable PDB (debugging symbols) are now embedded in the main library instead of being published as a separate NuGet symbols package (`.snupkg) (@kzu, [#1098](devlooped/moq#1098)) > > #### Fixed > > * `SetupProperty` fails if property getter and setter are not both defined in mocked type (@stakx, [#1017](devlooped/moq#1017)) > * Expression tree argument not matched when it contains a captured variable – evaluate all captures to their current values when comparing two expression trees (@QTom01, [#1054](devlooped/moq#1054)) > * Failure when parameterized `Mock.Of<>` is used in query comprehension `from` clause (@stakx, [#982](devlooped/moq#982)) > > > ## 4.15.0 > > This version was accidentally published as 4.15.1 due to an intermittent problem with NuGet publishing. #Commits - [`f2aa090`](devlooped/moq@f2aa090) ...
try this once, This also verifies your message as well. I found this implemented in one of my projects. logger.Verify(x => x.Log(LogLevel.Information, |
It's actually a lot easier to create a simple test Ilogger class instead of writing all those complex expressions or callbacks: public abstract class TestLogger : ILogger
{
...
public virtual void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
this.Log(logLevel, eventId, state, exception, formatter(state, exception));
}
public abstract void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, string message);
} and then it's easy to use: var mock = new Mock<TestLogger> { CallBase = true };
string expectedMessage = ...
...
mock.Verify(
x => x.Log(
LogLevel.Critical,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
null,
expectedMessage),
Times.Once); |
In .NET Core 2.2, I am able to use Moq to test calls made to an ILogger. For example the following code;
could be tested with the following Xunit Theory;
However when targeting .NET Core 3.0 (Preview 8), Moq now fails to verify these calls. The reason these calls now fail is because there was a behaviour change in that the Type that is being passed in to the
logger.Log()
generic has changed. It is now the Internal typeFormattedLogValues
.I noticed that Moq 4.13.0 introduced the generic type matcher
It.IsAnyType
. I tried to use it like this;Strangly it works in .NET Core 2.2 but is still failing in .NET Core 3.0 Preview 8.
The text was updated successfully, but these errors were encountered: