From 2cc6b791fb04d071bd1e388b53b25a969e62f9b4 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 19 Oct 2023 09:54:54 +0200 Subject: [PATCH] v1.2.0 with improvements of the BeginScope() method assertion. (#9) * Update the BeginScope() to use a delegate (#7). * Add a BeginScopeAsDictionary() to compare an object with a dictionary of string/object for the state (#8). --- README.md | 83 ++++++++- .../ILoggerMockSetupSequence.cs | 6 +- src/Logging.Assertions/LoggerMock.cs | 38 ++-- .../LoggerMockSetupSequenceExtensions.cs | 38 ++++ .../Logging.Assertions.csproj | 34 ++-- .../LoggerMockTest.cs | 166 +++++++++++++++++- 6 files changed, 334 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index c3c7b97..6b7074e 100644 --- a/README.md +++ b/README.md @@ -289,7 +289,88 @@ logger.SetupSequence() ... // Continue the setup expected log sequence ``` -### Assertion fail messages +### Test the BeginScope() state +If you use the `BeginScope` method in your logging process, you can assert the content of state +specified in argument using two methods. + +For example, to assert the following code: +``` +using (this.logger.BeginScope(new StateInfo() { Id = 1234 })) +{ + ... // Other Log +} +``` + +With the `StateInfo` class as simple like like that: +```csharp +public class StateInfo +{ + public int Id { get; set; } +} +``` + +You can assert the `BeginScope()` method call using an anonymous object: + +```csharp +var logger = new LoggerMock(); +logger.SetupSequence() + .BeginScope(new { Id = 1234 }) + ... // Other Log() assertions + .EndScope(); +``` + +> The `BeginScope()` assertion check the equivalence (property by property and not the reference itself) +between the actual object in the code and the expected object in the assertion. + +Or you can assert the `BeginScope()` method call using a delegate if your state object is complex: + +```csharp +var logger = new LoggerMock(); +logger.SetupSequence() + .BeginScope(state => + { + state.Id.Should().Be(1234); + }) + ... // Other Log() assertions + .EndScope(); +``` + +### Application Insights dictionary state +If you use Application Insights as output of your logs, the `BeginScope()` state argument must take a dictionary of string/object as the following code sample: + +``` +using (this.logger.BeginScope(new Dictionary() { { "Id", 1234 } })) +{ + ... // Other Log +} +``` + +To assert the `BeginScope()` in the previous sample code, you can use the `SetupSequence().BeginScope(Object)` method assertion as pass the expected +dictionary as argument. + +```csharp +var logger = new LoggerMock(); +logger.SetupSequence() + .BeginScope(new Dictionary() { { "Id", 1234 } }) + ... // Other Log() assertions + .EndScope(); +``` + +The [PosInformatique.Logging.Assertions](https://www.nuget.org/packages/PosInformatique.Logging.Assertions/) library provides a +`SetupSequence().BeginScopeAsDictionary(Object)` method which allows to assert the content of the dictionary using an object (Each property and his value of the expected +object is considered as a key/value couple of the dictionary). Do not hesitate to use anonymous object in your unit test to make the code more easy to read. + +The following example have the same behavior as the previous example, but is more easy to read by removing the dictionary instantiation and some extract brackets: + +```csharp +var logger = new LoggerMock(); +logger.SetupSequence() + .BeginScopeAsDictionary(new { Id = 1234 }) + ... // Other Log() assertions + .EndScope(); +``` + +## Assertion fail messages The [PosInformatique.Logging.Assertions](https://www.nuget.org/packages/PosInformatique.Logging.Assertions/) library try to make the assert fail messages the most easy to understand for the developers: diff --git a/src/Logging.Assertions/ILoggerMockSetupSequence.cs b/src/Logging.Assertions/ILoggerMockSetupSequence.cs index 3b1270a..f9d3ee5 100644 --- a/src/Logging.Assertions/ILoggerMockSetupSequence.cs +++ b/src/Logging.Assertions/ILoggerMockSetupSequence.cs @@ -16,9 +16,11 @@ public interface ILoggerMockSetupSequence /// /// Expect the call to the method. /// - /// State instance of the method argument expected. + /// Type of the state expected. + /// Delegate called to assert the content of the state when the + /// the is called. /// The current which allows to continue the setup of the method calls. - ILoggerMockSetupSequence BeginScope(object state); + ILoggerMockSetupSequence BeginScope(Action state); /// /// Expect the call to the method which represents the scope diff --git a/src/Logging.Assertions/LoggerMock.cs b/src/Logging.Assertions/LoggerMock.cs index 70a2a7c..cb1a2d8 100644 --- a/src/Logging.Assertions/LoggerMock.cs +++ b/src/Logging.Assertions/LoggerMock.cs @@ -7,6 +7,7 @@ namespace PosInformatique.Logging.Assertions { using System.Diagnostics.CodeAnalysis; + using System.Net.NetworkInformation; using FluentAssertions; using FluentAssertions.Common; using Microsoft.Extensions.Logging; @@ -78,9 +79,9 @@ public LoggerMockSetupSequence(LoggerMock mock) this.mock = mock; } - public ILoggerMockSetupSequence BeginScope(object state) + public ILoggerMockSetupSequence BeginScope(Action state) { - var logBeginScope = new ExpectedLogBeginScope(this, state); + var logBeginScope = new ExpectedLogBeginScope(this, state); this.mock.expectedLogActions.Add(logBeginScope); @@ -280,22 +281,39 @@ public Arguments(int count, Action action) } } - private sealed class ExpectedLogBeginScope : ExpectedLogAction + private sealed class ExpectedLogBeginScope : ExpectedLogBeginScope { - private readonly object expectedState; + private readonly Action assert; - public ExpectedLogBeginScope(LoggerMockSetupSequence sequence, object state) + public ExpectedLogBeginScope(LoggerMockSetupSequence sequence, Action state) : base(sequence) { - this.expectedState = state; + this.assert = state; } - public override string Name => "BeginScope"; + public override void Assert(TState state) + { + if (state is TExpectedState expectedState) + { + this.assert(expectedState); + } + else + { + Services.ThrowException($"The 'BeginScope()' has been called with a wrong state argument type (Expected: {typeof(TExpectedState).Name}, Actual: {typeof(TState).Name})."); + } + } + } - public void Assert(object? state) + private abstract class ExpectedLogBeginScope : ExpectedLogAction + { + protected ExpectedLogBeginScope(LoggerMockSetupSequence sequence) + : base(sequence) { - state.Should().BeEquivalentTo(this.expectedState); } + + public override string Name => "BeginScope"; + + public abstract void Assert(TState state); } private sealed class ExpectedLogEndScope : ExpectedLogAction @@ -321,7 +339,7 @@ protected ExpectedLogAction(LoggerMockSetupSequence sequence) public virtual string ExceptionDisplayMessage => this.Name; - public ILoggerMockSetupSequence BeginScope(object state) + public ILoggerMockSetupSequence BeginScope(Action state) { return this.sequence.BeginScope(state); } diff --git a/src/Logging.Assertions/LoggerMockSetupSequenceExtensions.cs b/src/Logging.Assertions/LoggerMockSetupSequenceExtensions.cs index 39dc533..e25b457 100644 --- a/src/Logging.Assertions/LoggerMockSetupSequenceExtensions.cs +++ b/src/Logging.Assertions/LoggerMockSetupSequenceExtensions.cs @@ -6,6 +6,7 @@ namespace PosInformatique.Logging.Assertions { + using System.Collections.Generic; using FluentAssertions; using Microsoft.Extensions.Logging; @@ -14,6 +15,43 @@ namespace PosInformatique.Logging.Assertions /// public static class LoggerMockSetupSequenceExtensions { + /// + /// Expect the call to the method with the specified object. + /// + /// to setup the sequence. + /// Expected state of the call. The state object actual and expected + /// are compared by equivalence (property by property) and not by instance. + /// The current which allows to continue the setup of the method calls. + public static ILoggerMockSetupSequence BeginScope(this ILoggerMockSetupSequence sequence, object state) + { + return sequence.BeginScope(expectedState => state.Should().BeEquivalentTo(expectedState)); + } + + /// + /// Expect the call to the method with a + /// of / represents by the object instance. + /// The dictionary is compared by all the public property of the specified object instance. + /// + /// to setup the sequence. + /// Expected state of the call. The properties of the expected object + /// is compared by a dictionary of / specified in the argument when calling the + /// method. + /// The current which allows to continue the setup of the method calls. + public static ILoggerMockSetupSequence BeginScopeAsDictionary(this ILoggerMockSetupSequence sequence, object state) + { + return sequence.BeginScope>(expectedState => + { + var actualState = new Dictionary(); + + foreach (var property in state.GetType().GetProperties()) + { + actualState.Add(property.Name, property.GetValue(state)); + } + + actualState.Should().BeEquivalentTo(expectedState); + }); + } + /// /// Expect the call to the method /// with a log level. diff --git a/src/Logging.Assertions/Logging.Assertions.csproj b/src/Logging.Assertions/Logging.Assertions.csproj index 0923415..dc771bf 100644 --- a/src/Logging.Assertions/Logging.Assertions.csproj +++ b/src/Logging.Assertions/Logging.Assertions.csproj @@ -10,21 +10,25 @@ https://github.com/PosInformatique/PosInformatique.Logging.Assertions README.md - 1.1.0 - - Add the support to assert the log message template and the parameters. - - 1.0.3 - - Use FluentAssertions 6.0.0 library. - - 1.0.2 - - The library target the .NET Standard 2.0 instead of .NET 6.0. - - 1.0.1 - - Various fixes for the NuGet package description. - - 1.0.0 - - Initial version - + 1.2.0 + - Improve the BeginScope() to use a delegate to assert complex state objects. + - Add a BeginScopeAsDictionary() method to check the state as dictionary string/object (useful for Application Insights logs). + + 1.1.0 + - Add the support to assert the log message template and the parameters. + + 1.0.3 + - Use FluentAssertions 6.0.0 library. + + 1.0.2 + - The library target the .NET Standard 2.0 instead of .NET 6.0. + + 1.0.1 + - Various fixes for the NuGet package description. + + 1.0.0 + - Initial version + logger log fluent unittest assert assertions logging mock diff --git a/tests/Logging.Assertions.Tests/LoggerMockTest.cs b/tests/Logging.Assertions.Tests/LoggerMockTest.cs index c5ec157..6bd164d 100644 --- a/tests/Logging.Assertions.Tests/LoggerMockTest.cs +++ b/tests/Logging.Assertions.Tests/LoggerMockTest.cs @@ -342,7 +342,7 @@ public void LoggerCalledToManyTimes() } [Fact] - public void VerifyAllLogs_WithScopes() + public void VerifyAllLogs_WithScopes_ObjectAssertion() { var logger = new LoggerMock(); logger.SetupSequence() @@ -364,7 +364,92 @@ public void VerifyAllLogs_WithScopes() } [Fact] - public void VerifyAllLogs_WithScopes_BeginScopeExpected() + public void VerifyAllLogs_WithScopes_DictionaryAssertion() + { + var logger = new LoggerMock(); + logger.SetupSequence() + .LogTrace("Log Trace 1") + .BeginScopeAsDictionary(new { ScopeLevel = 1, ScopeName = "Scope level 1" }) + .LogDebug("Log Debug 2") + .BeginScopeAsDictionary(new { ScopeLevel = 2, ScopeName = "Scope level 2" }) + .LogInformation("Log Information 3") + .EndScope() + .LogWarning("Log Warning 4") + .EndScope() + .LogError("Log Error 5"); + + var objectToLog = new ObjectToLog(logger.Object); + + objectToLog.InvokeWithScopeAsDictionary(); + + logger.VerifyLogs(); + } + + [Fact] + public void VerifyAllLogs_WithScopes_DelegateAssertion() + { + var logger = new LoggerMock(); + logger.SetupSequence() + .LogTrace("Log Trace 1") + .BeginScope(state => + { + state.ScopeLevel.Should().Be(1); + state.ScopeName.Should().Be("Scope level 1"); + }) + .LogDebug("Log Debug 2") + .BeginScope(state => + { + state.ScopeLevel.Should().Be(2); + state.ScopeName.Should().Be("Scope level 2"); + }) + .LogInformation("Log Information 3") + .EndScope() + .LogWarning("Log Warning 4") + .EndScope() + .LogError("Log Error 5"); + + var objectToLog = new ObjectToLog(logger.Object); + + objectToLog.InvokeWithScope(); + + logger.VerifyLogs(); + } + + [Fact] + public void BeginScope_DelegateAssertion_WrongExpectedStateType() + { + var logger = new LoggerMock(); + logger.SetupSequence() + .LogTrace("Log Trace 1") + .BeginScope(state => + { + throw new XunitException("Must not be called"); + }); + + var objectToLog = new ObjectToLog(logger.Object); + + objectToLog.Invoking(o => o.InvokeWithScope()) + .Should().ThrowExactly() + .WithMessage("The 'BeginScope()' has been called with a wrong state argument type (Expected: DateTime, Actual: State)."); + } + + [Fact] + public void BeginScope_AnonymousObjectAssertion_DifferentProperty() + { + var logger = new LoggerMock(); + logger.SetupSequence() + .LogTrace("Log Trace 1") + .BeginScope(new { DifferentProperty = "Other value" }); + + var objectToLog = new ObjectToLog(logger.Object); + + objectToLog.Invoking(o => o.InvokeWithScopeAsAnonymousObject()) + .Should().ThrowExactly() + .And.Message.StartsWith("Expectation has property state.ScopeLevel that the other object does not have.\r\nExpectation has property state.ScopeName that the other object does not have."); + } + + [Fact] + public void BeginScope_Expected() { var logger = new LoggerMock(); logger.SetupSequence() @@ -379,7 +464,7 @@ public void VerifyAllLogs_WithScopes_BeginScopeExpected() } [Fact] - public void VerifyAllLogs_WithScopes_EndScopeExpected() + public void BeginScope_EndScopeExpected() { var logger = new LoggerMock(); logger.SetupSequence() @@ -399,6 +484,36 @@ public void VerifyAllLogs_WithScopes_EndScopeExpected() .WithMessage("The 'Dispose()' method has been called but expected other action (Expected: Message)"); } + [Fact] + public void BeginScopeAsDictionary_ExpectedMissingProperty() + { + var logger = new LoggerMock(); + logger.SetupSequence() + .LogTrace("Log Trace 1") + .BeginScopeAsDictionary(new { ScopeLevel = 1, ScopeName = "Scope level 1", ExpectedProperty = "The expected value" }); + + var objectToLog = new ObjectToLog(logger.Object); + + objectToLog.Invoking(o => o.InvokeWithScopeAsDictionary()) + .Should().ThrowExactly() + .And.Message.StartsWith("Expected actualState to be a dictionary with 2 item(s), but has additional key(s) {\"ExpectedProperty\"}"); + } + + [Fact] + public void BeginScopeAsDictionary_ExpectedLessProperties() + { + var logger = new LoggerMock(); + logger.SetupSequence() + .LogTrace("Log Trace 1") + .BeginScopeAsDictionary(new { ScopeLevel = 1 }); + + var objectToLog = new ObjectToLog(logger.Object); + + objectToLog.Invoking(o => o.InvokeWithScopeAsDictionary()) + .Should().ThrowExactly() + .And.Message.StartsWith("Expected actualState to be a dictionary with 2 item(s), but it misses key(s) {\"ScopeName\"}"); + } + [Fact] public void IsEnabled_NotSupported() { @@ -451,6 +566,13 @@ public void LogError_WithException_ChainedWithLogError() logger.VerifyLogs(); } + private class State + { + public int ScopeLevel { get; set; } + + public string? ScopeName { get; set; } + } + private class ObjectToLog { private readonly ILogger logger; @@ -505,6 +627,25 @@ public void InvokeWithScope() { this.logger.LogTrace("Log Trace {0}", 1); + using (var scope1 = this.logger.BeginScope(new State { ScopeLevel = 1, ScopeName = "Scope level 1" })) + { + this.logger.LogDebug("Log Debug {0}", 2); + + using (var scope2 = this.logger.BeginScope(new State { ScopeLevel = 2, ScopeName = "Scope level 2" })) + { + this.logger.LogInformation("Log Information {0}", 3); + } + + this.logger.LogWarning("Log Warning {0}", 4); + } + + this.logger.LogError("Log Error {0}", 5); + } + + public void InvokeWithScopeAsAnonymousObject() + { + this.logger.LogTrace("Log Trace {0}", 1); + using (var scope1 = this.logger.BeginScope(new { ScopeLevel = 1, ScopeName = "Scope level 1" })) { this.logger.LogDebug("Log Debug {0}", 2); @@ -520,6 +661,25 @@ public void InvokeWithScope() this.logger.LogError("Log Error {0}", 5); } + public void InvokeWithScopeAsDictionary() + { + this.logger.LogTrace("Log Trace {0}", 1); + + using (var scope1 = this.logger.BeginScope(new Dictionary { { "ScopeLevel", 1 }, { "ScopeName", "Scope level 1" } })) + { + this.logger.LogDebug("Log Debug {0}", 2); + + using (var scope2 = this.logger.BeginScope(new Dictionary { { "ScopeLevel", 2 }, { "ScopeName", "Scope level 2" } })) + { + this.logger.LogInformation("Log Information {0}", 3); + } + + this.logger.LogWarning("Log Warning {0}", 4); + } + + this.logger.LogError("Log Error {0}", 5); + } + public void InvokeWithMessageTemplate() { this.logger.LogInformation("Log information with parameters {Id}, {Name} and {Object}", 1234, "The name", new { Property = "I am object" });