Skip to content

Commit

Permalink
Merge pull request #18 from sandermvanvliet/feature/assertion-improve…
Browse files Browse the repository at this point in the history
…ments

Assertion improvements
  • Loading branch information
sandermvanvliet authored May 10, 2021
2 parents 6728f94 + ba4d2bc commit c01a5d9
Show file tree
Hide file tree
Showing 10 changed files with 427 additions and 13 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,56 @@ InMemorySink.Instance
.NotHaveMessage("a specific message");
```

### Asserting properties on messages

When you want to assert that a message has a property you can do that using the `WithProperty` assertion:

```csharp
InMemorySink.Instance
.Should()
.HaveMessage("Message with {Property}")
.Appearing().Once()
.WithProperty("Property");
```

To then assert that it has a certain value you would use `WithValue`:

```csharp
InMemorySink.Instance
.Should()
.HaveMessage("Message with {Property}")
.Appearing().Once()
.WithProperty("Property")
.WithValue("property value");
```

Asserting that a message has multiple properties can be accomplished using the `And` constraint:

```csharp
InMemorySink.Instance
.Should()
.HaveMessage("Message with {Property1} and {Property2}")
.Appearing().Once()
.WithProperty("Property1")
.WithValue("value 1")
.And
.WithProperty("Property2")
.WithValue("value 2");
```

When you have a log message that appears a number of times and you want to assert that the value of the log property has the expected values you can do that using the `WithValues` assertion:

```csharp
InMemorySink.Instance
.Should()
.HaveMessage("Message with {Property1} and {Property2}")
.Appearing().Times(3)
.WithProperty("Property1")
.WithValue("value 1", "value 2", "value 3")
```

> **Note:** `WithValue` takes an array of values.
## Clearing log events between tests

Depending on your test framework and test setup you may want to ensure that the log events captured by the `InMemorySink` are cleared so tests
Expand Down
46 changes: 43 additions & 3 deletions src/Serilog.Sinks.InMemory.Assertions/InMemorySinkAssertions.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,57 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;
using Serilog.Events;

namespace Serilog.Sinks.InMemory.Assertions
{
public class InMemorySinkAssertions : ReferenceTypeAssertions<InMemorySink, InMemorySinkAssertions>
{
public InMemorySinkAssertions(InMemorySink instance)
{
Subject = instance;
Subject = SnapshotOf(instance);
}

/*
* Hack attack.
*
* This is a bit of a dirty way to work around snapshotting the InMemorySink instance
* to ensure that you won't get hit by an InvalidOperationException when calling
* HaveMessage() and the logger gets called from somewhere else and adds a new
* LogEvent to the collection while that method is invoked.
*
* For now we copy the LogEvents from the current sink and use reflection to assign
* it to a new instance of InMemorySink that will be used by the assertions,
* effectively snapshotting the InMemorySink that was used by the tests.
*/
private static InMemorySink SnapshotOf(InMemorySink instance)
{
// Capture the current log events
var logEvents = instance.LogEvents.ToList();

// Create a new sink instance
var snapshot = new InMemorySink();

// Get the field that holds the collection of log events
var field = snapshot.GetType().GetField("_logEvents", BindingFlags.NonPublic | BindingFlags.Instance);

if (field == null)
{
throw new InvalidOperationException(
"Can't snapshot the InMemorySink instance because the private field that holds the messages could not be found.");
}

// Assign the snapshot of log events to the field
((List<LogEvent>)field.GetValue(snapshot)).AddRange(logEvents);

// Return the new snapshotted InMemorySink instance
return snapshot;
}

protected override string Identifier { get; } = nameof(InMemorySink);
protected override string Identifier => nameof(InMemorySink);

public LogEventsAssertions HaveMessage(
string messageTemplate,
Expand Down
6 changes: 3 additions & 3 deletions src/Serilog.Sinks.InMemory.Assertions/LogEventAssertion.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using FluentAssertions.Execution;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;
using Serilog.Events;

Expand All @@ -15,7 +14,7 @@ public LogEventAssertion(string messageTemplate, LogEvent subject)
Subject = subject;
}

protected override string Identifier { get; } = "log event";
protected override string Identifier => "log event";

public LogEventPropertyValueAssertions WithProperty(string name, string because = "", params object[] becauseArgs)
{
Expand All @@ -27,6 +26,7 @@ public LogEventPropertyValueAssertions WithProperty(string name, string because
name);

return new LogEventPropertyValueAssertions(
this,
Subject.Properties[name],
name);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
using FluentAssertions.Execution;
using FluentAssertions;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;
using Serilog.Events;

namespace Serilog.Sinks.InMemory.Assertions
{
public class LogEventPropertyValueAssertions : ReferenceTypeAssertions<LogEventPropertyValue, LogEventPropertyValueAssertions>
{
public LogEventPropertyValueAssertions(LogEventPropertyValue instance, string propertyName)
private readonly LogEventAssertion _logEventAssertion;

public LogEventPropertyValueAssertions(LogEventAssertion logEventAssertion, LogEventPropertyValue instance, string propertyName)
{
_logEventAssertion = logEventAssertion;
Subject = instance;
Identifier = propertyName;
}

protected override string Identifier { get; }

public void WithValue(object value, string because = "", params object[] becauseArgs)
public AndConstraint<LogEventAssertion> WithValue(object value, string because = "", params object[] becauseArgs)
{
var actualValue = GetValueFromProperty(Subject);

Expand All @@ -25,6 +29,8 @@ public void WithValue(object value, string because = "", params object[] because
Identifier,
value,
actualValue);

return new AndConstraint<LogEventAssertion>(_logEventAssertion);
}

private object GetValueFromProperty(LogEventPropertyValue instance)
Expand Down
15 changes: 13 additions & 2 deletions src/Serilog.Sinks.InMemory.Assertions/LogEventsAssertions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;
Expand Down Expand Up @@ -75,5 +74,17 @@ public LogEventsAssertions WithLevel(LogEventLevel level, string because = "", p

return this;
}

public LogEventsPropertyAssertion WithProperty(string propertyName, string because = "", params object[] becauseArgs)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.ForCondition(Subject.All(logEvent => logEvent.Properties.ContainsKey(propertyName)))
.FailWith($"Expected all instances of log message {{0}} to have property {{1}}, but it was not found",
_messageTemplate,
propertyName);

return new LogEventsPropertyAssertion(this, propertyName);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;
using Serilog.Events;

namespace Serilog.Sinks.InMemory.Assertions
{
public class LogEventsPropertyAssertion : ReferenceTypeAssertions<IEnumerable<LogEvent>, LogEventsPropertyAssertion>
{
private readonly LogEventsAssertions _logEventsAssertions;
private readonly IEnumerable<LogEvent> _logEvents;

public LogEventsPropertyAssertion(LogEventsAssertions logEventsAssertions, string propertyName)
{
_logEventsAssertions = logEventsAssertions;
_logEvents = logEventsAssertions.Subject;
Identifier = propertyName;
}

protected override string Identifier { get; }

public AndConstraint<LogEventsAssertions> WithValues(params object[] values)
{
Execute.Assertion
.ForCondition(_logEvents.Count() == values.Length)
.FailWith(
$"Can't assert property values because {values.Length} values were provided while only {_logEvents.Count()} messages were expected");

var propertyValues = _logEvents
.Select(logEvent => GetValueFromProperty(logEvent.Properties[Identifier]))
.ToArray();

var notFound = values
.Where(v => !propertyValues.Contains(v))
.ToArray();

Execute.Assertion
.ForCondition(!notFound.Any())
.FailWith("Expected property values {0} to contain {1} but did not find {2}",
propertyValues,
values,
notFound);

return new AndConstraint<LogEventsAssertions>(_logEventsAssertions);
}

private object GetValueFromProperty(LogEventPropertyValue instance)
{
switch(instance)
{
case ScalarValue scalarValue:
return scalarValue.Value;
default:
return Subject.ToString();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<PropertyGroup>
<IsPackable>true</IsPackable>
<Version>0.6.0</Version>
<Version>0.7.0</Version>
<Title>Serilog in-memory sink assertion extensions</Title>
<Description>FluentAssertions extensions to use with the Serilog.Sinks.InMemory package</Description>
<Copyright>2020 Sander van Vliet</Copyright>
Expand All @@ -23,7 +23,9 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.*" />
<PackageReference Include="Serilog.Sinks.InMemory" Version="0.*" />
<PackageReference Include="Serilog.Sinks.InMemory" Version="0.*">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Serilog" Version="2.*">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Xunit;

namespace Serilog.Sinks.InMemory.Assertions.Tests.Unit
{
public class WhenAssertingAndSInkIsWrittenTo
{
[Fact]
public void AssertionShouldNotFailWhenInstanceIsLoggedToAfterInvokingTheAssertion()
{
/*
* This test is meant to verify the behaviour of the assertions
* when a message is logged to the instance we're asserting on
* after we invoked the assertion.
*
* The behaviour is that when calling Should(), the assertion
* should (pun intended) capture a snapshot of the log events
* at the point in time it was invoked.
*
* This prevents InvalidOperationExceptions because the internal
* collection is modified.
*
* See https://github.com/sandermvanvliet/SerilogSinksInMemory/issues/16
*/
var sink = new InMemorySink();

var logger = new LoggerConfiguration()
.WriteTo.Sink(sink)
.CreateLogger();

// Log 2 messages
logger.Information("Message");
logger.Information("Message");

// Start assertion
var assertion = sink.Should();

// Pretend another thread/task/fiber logs another message
logger.Information("Message");

// Continue with the assertion
assertion
.HaveMessage("Message")
.Appearing().Times(2);
}
}
}
Loading

0 comments on commit c01a5d9

Please sign in to comment.