Capture and CaptureMatch
Last time
I removed some code that was problematic with regard to thread safety.
While refactoring, I also touched a few lines in the Setup.Execute
method:
+// update matchers (important for `Capture`):
-// update condition (important for `MockSequence`) and matchers (important for `Capture`):
-this.Condition?.SetupEvaluatedSuccessfully();
this.expectation.SetupEvaluatedSuccessfully(invocation);
Two things bother me here:
-
One
SetupEvaluatedSucessfully
calls gets removed, but one remains. Given that both methods are named identically, do they also have the same function? Does that mean that another thread-safety issue has remained unresolved? -
The comment mentions argument matchers being updated. That seems wrong. Argument matchers should not have any side effects!
So let’s take a closer look at the mentioned Capture
class.
What is it?
Capture
is a static class like It
that contains argument matcher
methods:
-
Capture.In(collection)
acts likeIt.IsAny<T>()
, but additionally it will add the inspected argument value to a givencollection
if the setup matches an invocation:var values = new List<string>(); mock.Setup(m => m.OnNumber(Capture.In(values))); mock.Setup(m => m.OnString(Capture.In(values))); mock.Object.OnNumber("42"); mock.Object.OnString("Alice"); Assert.Equal(new[] { "42", "Alice" }, values);
-
Capture.With(captureMatch)
follows the same idea, but offers more flexibility: user code can specify what should happen with argument values (instead of just defaulting to adding to a collection), and optionally when argument values match (instead of defaulting toIt.IsAny<T>()
behavior):var n = 0; var logA = new CaptureMatch<string>( arg => Console.WriteLine(arg), arg => arg.StartsWith("A")); mock.Setup(m => m.OnString(Capture.With(logA))).Callback(() => n++); mock.Object.OnString("Alice"); // should print Alice mock.Object.OnString("Bob"); // not mached Assert.Equal(1, n);
What’s wrong with it?
Conceptually, matchers are simple predicates: they take a value, run a
test on it, and report the test result (a truth value, i.e. true
or
false
). They shouldn’t need to mutate state and thus cause visible
side effects in order to do this.
What’s worse here is that extra machinery is needed for Capture
: it
needs to be able to only update state (like an external collection) when
the setup that makes use of it is actually matched.
In that respect, CaptureMatch
is very similar to the Condition
class
we looked at in the last post: it keeps a test function, and something
like a success
delegate, which needs to be invoked once the setup
is matched. So, just as with conditional setups, there must be a thread-
safety issue lurking around here.
Alternatives
Code using Capture.In
can be rewritten using It.IsAny<>()
and
code in .Callback
to add the value to a collection:
values = new List<string>();
mock.Setup(m => m.OnNumber(It.IsAny<string>())).Callback(values.Add);
mock.Setup(m => m.OnString(It.IsAny<string>())).Callback(values.Add);
CaptureMatch
can be similarly rewritten using It.Is<>(condition)
and any code in .Callback
.
Removal
The two classes are easily removed; see 5b15506
. The more satisfying
part here is removing all supporting internal machinery, i.e.
SetupSuccessfullyEvaluated
(b549261
): that method has sent its
tendrils into quite a few places in the code base, even verification.
(Regarding that last one, see moq/moq4#968
for an ingenious previous use case for Capture.In
.)