I'm using the excellent Shouldly
library in my xUnit tests and I'm finding myself using the set sequence of assertions in different tests, so I'm combining them into new assertion extension methods - but when I do this I lose Shouldly
's contextual assertion messages.
Here's my old code, which works with Shouldly
to include source-level information and call-site context in the Shouldly
assertion error:
[Fact]
public void Dict_should_contain_valid_Foobar_Bar_entry()
{
IDictionary<String,Bar> dict = ...
dict.TryGetValue( "Foobar", out Bar bar ).ShouldBeTrue();
bar.ShouldNotBeNull();
bar.ChildList.Count.ShouldBe( expected: 3 );
bar.Message.ShouldBeNull();
}
[Fact]
public void Dict_should_contain_valid_Barbaz_Bar_entry()
{
IDictionary<String,Bar> dict = ...
dict.TryGetValue( "Barbaz", out Bar bar ).ShouldBeTrue();
bar.ShouldNotBeNull();
bar.ChildList.Count.ShouldBe( expected: 3 );
bar.Message.ShouldBeNull();
}
I converted it to this new extension method in the same project:
public static void ShouldBeValidBar( this IDictionary<String,Bar> dict, String barName )
{
dict.ShouldNotBeNull();
dict.TryGetValue( barName, out Bar bar ).ShouldBeTrue();
bar.ShouldNotBeNull();
bar.ChildList.Count.ShouldBe( expected: 3 );
bar.Message.ShouldBeNull();
}
And so my tests are changed to this:
[Fact]
public void Dict_should_contain_valid_Foobar_Bar_entry()
{
IDictionary<String,Bar> dict = ...
dict.ShouldBeValidBar( "Foobar" );
}
[Fact]
public void Dict_should_contain_valid_Barbaz_Bar_entry()
{
IDictionary<String,Bar> dict = ...
dict.ShouldBeValidBar( "Barbaz" );
}
...but now my Shouldly assert messages don't contain any contextual information from Dict_should_contain_valid_Foobar_Bar_entry
and instead only contains context from ShouldBeValidBar
.
How can I instruct Shouldly
to disregard the context of ShouldBeValidBar
and to use its parent call-site instead?
Add the [ShouldlyMethods]
attribute to your custom assert extension method classes (not individual extension methods):
[ShouldlyMethods] // <-- This, right here!
public static class MyShouldlyAssertions
{
public static void ShouldBeValidBar( this IDictionary<String,Bar> dict, String barName )
{
[...]
}
}
After some googling, and reading articles about how Shouldly
works - and after reading the source of Shouldly's secret-sauce: SourceCodeTextGetter
, I see that it determines which entries in the stack-trace can be overlooked by the presence of the [ShouldlyMethods]
attribute (Shouldly.ShouldlyMethodsAttribute
) on the method's containing type in each frame of the stack-trace:
void ParseStackTrace(StackTrace trace)
{
[...]
while (ShouldlyFrame == null || currentFrame.GetMethod().IsShouldlyMethod())
{
if (currentFrame.GetMethod().IsShouldlyMethod())
ShouldlyFrame = currentFrame;
[...]
}
[...]
}
internal static bool IsShouldlyMethod(this MethodBase method)
{
if (method.DeclaringType == null)
return false;
return
method
.DeclaringType
.GetCustomAttributes( typeof(ShouldlyMethodsAttribute), true )
.Any()
||
(
method.DeclaringType.DeclaringType != null
&&
method
.DeclaringType
.DeclaringType
.GetCustomAttributes( typeof(ShouldlyMethodsAttribute), true )
.Any()
);
}
So it's just a matter of adding the [ShouldlyMethods]
attribute to my extension methods' container classes:
[ShouldlyMethods]
public static class ShouldlyAssertionExtensions
{
public static void ShouldBeValidBar( this IDictionary<String,Bar> dict, String barName )
{
dict.ShouldNotBeNull();
dict.TryGetValue( barName, out Bar bar ).ShouldBeTrue();
bar.ShouldNotBeNull();
bar.ChildList.Count.ShouldBe( expected: 3 );
bar.Message.ShouldBeNull();
}
}
And now my assert errors have the context of the call-site of ShouldBeValidBar
. Hurrah!