How does the Shouldly assertion library know the expression the assertion was applied to?
Asked Answered
R

2

22

The Shouldly assertion library for .NET somehow knows what expression the assertion method was called on so it is able to display it into the message. I tried to find out how it works but got lost in the source code. I suspect it looks into the compiled code but I would really like to see how this happens. From the documentation

map.IndexOfValue("boo").ShouldBe(2); // -> map.IndexOfValue("boo") should be 2 but was 1

Somehow Shouldly knows the expression map.IndexOfValue("boo") and was able to display it in the test failure message. Does anyone know how this happens?

Rickettsia answered 10/10, 2016 at 13:19 Comment(0)
M
17

Looking at the code, that's pretty smart.

The magic is happening in the ActualCodeTextGetter class. First, it retrieves the line of the source code file by using the StackTrace:

  StackTrace stackTrace = trace ?? new StackTrace(true);

  // Cut for brevity

  StackFrame stackFrame = frame;
  this.ShouldlyFrameIndex = index - 1;
  string fileName = stackFrame.GetFileName();
  this._determinedOriginatingFrame = fileName != null && File.Exists(fileName);
  this._shouldMethod = this.ShouldlyFrame.GetMethod().Name;
  this.FileName = fileName;
  this.LineNumber = stackFrame.GetFileLineNumber() - 1;

Once it has the name of the source code file, along with the line and offset of the statement, it's just a matter of reading directly the file:

private string GetCodePart()
{
  string str = "Shouldly uses your source code to generate its great error messages, build your test project with full debug information to get better error messages\nThe provided expression";
  if (this._determinedOriginatingFrame)
  {
    string codeLines = string.Join("\n", ((IEnumerable<string>) File.ReadAllLines(this.FileName)).Skip<string>(this.LineNumber).ToArray<string>());
    int indexOfMethod = codeLines.IndexOf(this._shouldMethod);
    if (indexOfMethod > 0)
      str = codeLines.Substring(0, indexOfMethod - 1).Trim();
    str = !str.EndsWith("Should") ? str.RemoveVariableAssignment().RemoveBlock() : this.GetCodePartFromParameter(indexOfMethod, codeLines, str);
  }
  return str;
}

There's a lot more logic going on to isolate precisely the statement, but in short the trick is:

  • Use the StackTrace to retrieve the location of the source code and the line of the statement
  • Parse the source code to retrieve the exact statement

Of course, it can work only if you're running it on the same machine you used to compile the code.

Mimosa answered 10/10, 2016 at 13:36 Comment(1)
This is a very old question but I still encounter this message. How does building with full debug information have anything to do with removing that message? Unless, as @kevin-gosse alluded to, the source file is in the exact same location as on the machine on which it was compiled, the message will still show up for me, with Shouldly 4.0.3 and .NET 4.7.2.Bryozoan
M
3

It does it by parsing the stack trace for the current call stack until it finds a method that is not internal to Shouldly and treats its name as the name of the method under test.

Maddux answered 10/10, 2016 at 13:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.