Skip to content

Commit

Permalink
v1.2.0 with improvements of the BeginScope() method assertion. (#9)
Browse files Browse the repository at this point in the history
* 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).
  • Loading branch information
GillesTourreau authored Oct 19, 2023
1 parent 72d5c4f commit 2cc6b79
Show file tree
Hide file tree
Showing 6 changed files with 334 additions and 31 deletions.
83 changes: 82 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<CustomerManager>();
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<CustomerManager>();
logger.SetupSequence()
.BeginScope<State>(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<string, object>() { { "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<CustomerManager>();
logger.SetupSequence()
.BeginScope(new Dictionary<string, object>() { { "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<CustomerManager>();
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:

Expand Down
6 changes: 4 additions & 2 deletions src/Logging.Assertions/ILoggerMockSetupSequence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ public interface ILoggerMockSetupSequence
/// <summary>
/// Expect the call to the <see cref="ILogger.BeginScope{TState}(TState)"/> method.
/// </summary>
/// <param name="state">State instance of the <see cref="BeginScope(object)"/> method argument expected.</param>
/// <typeparam name="TState">Type of the state expected.</typeparam>
/// <param name="state">Delegate called to assert the content of the state when the
/// the <see cref="ILogger.BeginScope{TState}(TState)"/> is called.</param>
/// <returns>The current <see cref="ILoggerMockSetupSequence"/> which allows to continue the setup of the <see cref="ILogger"/> method calls.</returns>
ILoggerMockSetupSequence BeginScope(object state);
ILoggerMockSetupSequence BeginScope<TState>(Action<TState> state);

/// <summary>
/// Expect the call to the <see cref="IDisposable.Dispose"/> method which represents the scope
Expand Down
38 changes: 28 additions & 10 deletions src/Logging.Assertions/LoggerMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -78,9 +79,9 @@ public LoggerMockSetupSequence(LoggerMock<TCategoryName> mock)
this.mock = mock;
}

public ILoggerMockSetupSequence BeginScope(object state)
public ILoggerMockSetupSequence BeginScope<TState>(Action<TState> state)
{
var logBeginScope = new ExpectedLogBeginScope(this, state);
var logBeginScope = new ExpectedLogBeginScope<TState>(this, state);

this.mock.expectedLogActions.Add(logBeginScope);

Expand Down Expand Up @@ -280,22 +281,39 @@ public Arguments(int count, Action<LogMessageTemplateArguments> action)
}
}

private sealed class ExpectedLogBeginScope : ExpectedLogAction
private sealed class ExpectedLogBeginScope<TExpectedState> : ExpectedLogBeginScope
{
private readonly object expectedState;
private readonly Action<TExpectedState> assert;

public ExpectedLogBeginScope(LoggerMockSetupSequence sequence, object state)
public ExpectedLogBeginScope(LoggerMockSetupSequence sequence, Action<TExpectedState> state)
: base(sequence)
{
this.expectedState = state;
this.assert = state;
}

public override string Name => "BeginScope";
public override void Assert<TState>(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>(TState state);
}

private sealed class ExpectedLogEndScope : ExpectedLogAction
Expand All @@ -321,7 +339,7 @@ protected ExpectedLogAction(LoggerMockSetupSequence sequence)

public virtual string ExceptionDisplayMessage => this.Name;

public ILoggerMockSetupSequence BeginScope(object state)
public ILoggerMockSetupSequence BeginScope<TState>(Action<TState> state)
{
return this.sequence.BeginScope(state);
}
Expand Down
38 changes: 38 additions & 0 deletions src/Logging.Assertions/LoggerMockSetupSequenceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace PosInformatique.Logging.Assertions
{
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.Extensions.Logging;

Expand All @@ -14,6 +15,43 @@ namespace PosInformatique.Logging.Assertions
/// </summary>
public static class LoggerMockSetupSequenceExtensions
{
/// <summary>
/// Expect the call to the <see cref="ILogger.BeginScope{TState}(TState)"/> method with the specified <paramref name="state"/> object.
/// </summary>
/// <param name="sequence"><see cref="ILoggerMockSetupSequence"/> to setup the sequence.</param>
/// <param name="state">Expected state of the <see cref="ILogger.BeginScope{TState}(TState)"/> call. The state object actual and expected
/// are compared by equivalence (property by property) and not by instance.</param>
/// <returns>The current <see cref="ILoggerMockSetupSequence"/> which allows to continue the setup of the <see cref="ILogger"/> method calls.</returns>
public static ILoggerMockSetupSequence BeginScope(this ILoggerMockSetupSequence sequence, object state)
{
return sequence.BeginScope<object>(expectedState => state.Should().BeEquivalentTo(expectedState));
}

/// <summary>
/// Expect the call to the <see cref="ILogger.BeginScope{TState}(TState)"/> method with a <see cref="Dictionary{TKey, TValue}"/>
/// of <see cref="string"/>/<see cref="object"/> represents by the <paramref name="state"/> object instance.
/// The dictionary is compared by all the public property of the specified <paramref name="state"/> object instance.
/// </summary>
/// <param name="sequence"><see cref="ILoggerMockSetupSequence"/> to setup the sequence.</param>
/// <param name="state">Expected state of the <see cref="ILogger.BeginScope{TState}(TState)"/> call. The properties of the expected object <paramref name="state"/>
/// is compared by a dictionary of <see cref="string"/>/<see cref="object"/> specified in the argument when calling the
/// <see cref="ILogger.BeginScope{TState}(TState)"/> method.</param>
/// <returns>The current <see cref="ILoggerMockSetupSequence"/> which allows to continue the setup of the <see cref="ILogger"/> method calls.</returns>
public static ILoggerMockSetupSequence BeginScopeAsDictionary(this ILoggerMockSetupSequence sequence, object state)
{
return sequence.BeginScope<IDictionary<string, object>>(expectedState =>
{
var actualState = new Dictionary<string, object>();

foreach (var property in state.GetType().GetProperties())
{
actualState.Add(property.Name, property.GetValue(state));
}

actualState.Should().BeEquivalentTo(expectedState);
});
}

/// <summary>
/// Expect the call to the <see cref="ILogger.Log{TState}(LogLevel, EventId, TState, Exception?, Func{TState, Exception?, string})"/> method
/// with a <see cref="LogLevel.Debug"/> log level.
Expand Down
34 changes: 19 additions & 15 deletions src/Logging.Assertions/Logging.Assertions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,25 @@
<PackageProjectUrl>https://github.com/PosInformatique/PosInformatique.Logging.Assertions</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>
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
</PackageReleaseNotes>
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
</PackageReleaseNotes>
<PackageTags>logger log fluent unittest assert assertions logging mock</PackageTags>
</PropertyGroup>

Expand Down
Loading

0 comments on commit 2cc6b79

Please sign in to comment.