Formatting Argument Values
FakeItEasy tries to provide helpful error messages when an Assertion isn't met. For example, when an expected call to a fake method isn't made, or when an unexpected call is made. Often these messages are adequate, but sometimes there's a need to improve upon them, which can be done by writing custom argument value formatters.
Writing a custom argument value formatter
Just define a class that extends FakeItEasy.ArgumentValueFormatter<T>
. Here's a sample that formats argument values of type Book
:
class BookArgumentValueFormatter : ArgumentValueFormatter<Book>
{
protected override string GetStringValue(Book argumentValue)
{
return string.Format("'{0}' published on {1:yyyy-MM-dd}",
argumentValue.Title, argumentValue.PublishedOn);
}
}
This would help FakeItEasy display this error message:
Assertion failed for the following call: SampleTests.ILibrary.Checkout() Expected to find it never but found it #1 times among the calls: 1: SampleTests.ILibrary.Checkout(book: 'The Ocean at the End of the Lane', published on 2013-06-18)
which could make tracking down any failures a little easier.
Compare to the original behaviour:
Assertion failed for the following call: SampleTests.ILibrary.Checkout() Expected to find it never but found it #1 times among the calls: 1: SampleTests.ILibrary.Checkout(book: SampleTests.Book)
In the original form of the message, the Book argument is just
formatted using Book.ToString()
because FakeItEasy doesn't know any
better.
How it works
FakeItEasy uses classes that implement the following interface to format argument values:
public interface IArgumentValueFormatter
{
string GetArgumentValueAsString(object argumentValue);
Type ForType { get; }
int Priority { get; }
}
GetArgumentValueAsString
does the work, transforming an argument into its formatted representation.
ForType
indicates what type of argument a formatter can format.
Priority
is discussed below.
Above, we wrote a formatter in the preferred way, by extending
abstract class ArgumentValueFormatter<T>:
IArgumentValueFormatter
. ArgumentValueFormatter<T>
defines a
GetArgumentValueAsString
method that defers to GetStringValue
, and
its ForType
method simply returns T
. The default implementation of
Priority
returns int.MinValue
, but this can be overridden. It's
possible to write a formatter from scratch, but there's no advantage
to doing so over extending ArgumentValueFormatter<T>
.
It's possible to create formatters for any type, including concrete types, abstract types, and interfaces. Formatters defined for base types and interfaces will be used when formatting values whose types extend or implement the formatter's type.
FakeItEasy's default formatter behaviour
Unless custom formatters are provided, FakeItEasy formats argument values like so:
- the
null
value is formatted as<null>
, - the empty
string
is formatted asstring.Empty
, - other
string
values are formatted as"the string value"
, including the quotation marks, and - any other value is formatted as its
ToString()
result
There is no way to change FakeItEasy's behaviour when formatting
null
, but the other behaviour can be overridden by user-defined
formatters.
Resolving formatter collisions
It's possible for a solution to contain multiple formatters that would
apply to the same types of arguments. In fact, it's guaranteed to
happen, since FakeItEasy itself defines a formatter that applies to
object
s and one that applies to string
s. Any user-defined
formatter will conflict with at least the built-in object formatter,
and maybe others. When there is more than one candidate for formatting
an argument, FakeItEasy picks the best one based on two factors:
- the distance between the argument's type (hereafter ArgType) and the type each formatter knows about (hereafter ForType), and
- the value of each formatter's
Priority
property
Lowest distance
When an argument value needs to be formatted, FakeItEasy examines all known formatters whose ForType is in ArgType's inheritance tree, or whose ForType is an interface that ArgType implements. The distance between ForType and ArgType is calculated as follows:
- 0 if ForType and ArgType are the same
- 1 if ForType is an interface that ArgType implements1
- 2 if
ForType == ArgType.BaseType
, - 3 if
ForType == ArgType.BaseType.BaseType
, and so on, adding one for every step in the inheritance chain
The formatter whose ForType has the smallest distance to ArgType is used to format the argument.
Highest priority
Sometimes more than one formatter is found the same distance from
ArgType. Maybe two formatters actually specify the same ForType
property value, or there's a formatter defined for ArgType as well as
for an interface that ArgType implements.
When multiple formatters have the same distance from the argument,
FakeItEasy will select the one with the highest Priority
property
value. If multiple formatters have the same distance and the same
priority, the behavior is undefined.
The formatters that FakeItEasy includes have Priority
equal to
int.MinValue
, as do all classes that extend
ArgumentValueFormatter<T>
, unless they explicitly override the
property. So, for example, a user-provided alternate formatter for
string
s should override Priority
, having it return a higher
value. Otherwise, there's no guarantee which formatter will be used.
How does FakeItEasy find Argument Value Formatters?
On initialization, FakeItEasy looks for Discoverable Extension Points, including Argument Value Formatters.
- In FakeItEasy 1.13.1 and earlier, the distance
0
was returned if the ForType and ArgType were the same or if ForType was an interface that ArgType implements, so if there were two formatters, each matching one of those conditions, there was no way to tell which one would be used. This was fixed in Issue 142.