Reimplement SetupSequence to make it thread-safe#476
Conversation
stakx
left a comment
There was a problem hiding this comment.
This is a draft and needs more work.
| { | ||
| // keeping the fluent API separate from `SequenceMethodCall` saves us from having to | ||
| // define a generic variant `SequenceMethodCallReturn<TResult>`, which would be much more | ||
| // work that having a generic fluent API counterpart `SequenceMethodCall<TResult>`. |
There was a problem hiding this comment.
This demonstrates a pattern that should probably be applied more in Moq. Moq's "plumbing" classes (e.g. MethodCall, MethodCallReturn<TResult>, Mock) should be made largely non-generic and kept strictly separate from the generic classes that are part of the fluent API. These latter types only flow the context of a fluent API "phrase" (i.e. a chain of method calls starting with mock.Setup… and ending with the end-of-line semicolon). That way, we could probably get rid of some code duplication.
|
|
||
| public ISetupSequentialAction Throws<TException>() | ||
| where TException : Exception, new() | ||
| => this.Throws(new TException()); |
There was a problem hiding this comment.
Perhaps delayed instantiation should be preferred over newing up exception objects way in advance?
| /// building the intermediate hierarchy of mock objects if necessary. | ||
| /// </summary> | ||
| private static Interceptor GetInterceptor(Expression fluentExpression, Mock mock) | ||
| internal static Interceptor GetInterceptor(Expression fluentExpression, Mock mock) |
There was a problem hiding this comment.
Making this and the following helper methods internal was a quick hack to see if the reimplementation was feasible at all. This needs to be cleaned up. Do these methods actually have to be located in the Mock class?
| using System.Reflection; | ||
| using Moq.Language; | ||
| using Moq.Language.Flow; | ||
| using System.Linq; |
There was a problem hiding this comment.
(Detail:) Sort usings properly.
| @@ -0,0 +1,65 @@ | |||
| using System; | |||
There was a problem hiding this comment.
(Detail:) Add missing license text to all new files!
| { | ||
| // we get here if there are more invocations than configured responses. | ||
| // if the setup method does not have a return value, we don't need to do anything; | ||
| // if it does have a return value, we produce the default value. |
There was a problem hiding this comment.
Verify (via additional unit tests) that the new implementation acts the same way as the old implementation when the configured responses have been exhausted by too many invocations.
There was a problem hiding this comment.
The new implementation acts differently; see explanation in #467 (comment). I personally favour the new behaviour as it brings SetupSequence more in line with how other Setup methods work: unless a setup is a conditional setup (.When(...)), then it overrides any previous setup for the same invocation.
| } | ||
| else if (call.Method.ReturnType.GetTypeInfo().IsValueType) | ||
| { | ||
| call.ReturnValue = Activator.CreateInstance(call.Method.ReturnType); |
There was a problem hiding this comment.
AFAIK other parts of Moq do this in a more complicated fashion, because of nullable value types, COM types, etc. Check whether this is sufficient.
| } | ||
|
|
||
| [Fact] | ||
| public async Task Test() |
There was a problem hiding this comment.
Give this test a more sensible name.
| for (int i = 1; i <= length; i++) | ||
| { | ||
| seqSetup = seqSetup | ||
| .Returns(i); |
There was a problem hiding this comment.
Unnecessary line break, reformat.
| var orderedInvocations = invocationsQueue.OrderBy(x => x).ToArray(); | ||
|
|
||
| // The assertion prints the real invocations, you will see duplicates there! | ||
| Assert.Equal( |
There was a problem hiding this comment.
@icalvo's original repro code used MSTest's CollectionAssert.AreEqual. Make sure that this is indeed xUnit.NET's way of comparing two sequences.
d43a177 to
5646a64
Compare
stakx
left a comment
There was a problem hiding this comment.
Minor points from previous issue have been addressed. Need to add additional unit tests now that try to detect changes in behaviour.
| /// </summary> | ||
| [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "By Design")] | ||
| public ISetupSequentialResult<TResult> SetupSequence<TResult>( | ||
| Expression<Func<T, TResult>> expression) |
There was a problem hiding this comment.
Unnecessary line break, remove.
|
|
||
| [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "By Design")] | ||
| internal static SetupSequencePhrase<TResult> SetupSequence<T, TResult>(Mock<T> mock, Expression<Func<T, TResult>> expression) | ||
| where T : class |
There was a problem hiding this comment.
Does this method really have to be generic? Why not make the parameters be of types Mock and LambdaExpression? The public method in Mock<T> can make sure that everything is appropriately typed.
| } | ||
|
|
||
| internal static SetupSequencePhrase SetupSequence<T>(Mock<T> mock, Expression<Action<T>> expression) | ||
| where T : class |
There was a problem hiding this comment.
Same here: Does this method really have to be generic? Why not make the parameters be of types Mock and LambdaExpression? The public method in Mock<T> can make sure that everything is appropriately typed.
| public static ISetupSequentialResult<TResult> SetupSequence<TMock, TResult>( | ||
| this Mock<TMock> mock, | ||
| Expression<Func<TMock, TResult>> expression) | ||
| where TMock : class |
There was a problem hiding this comment.
TMock should be just T, but changing a generic type parameter's name is possibly a (minor) breaking change. Leave as is for now.
| [Obsolete("Please use instance method Mock<T>.SetupSequence instead.")] | ||
| [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "By Design")] | ||
| public static ISetupSequentialAction SetupSequence<TMock>(this Mock<TMock> mock, Expression<Action<TMock>> expression) | ||
| where TMock : class |
There was a problem hiding this comment.
Same here: TMock should be just T, but changing a generic type parameter's name is possibly a (minor) breaking change. Leave as is for now.
5b366a9 to
3e5564e
Compare
stakx
left a comment
There was a problem hiding this comment.
All points have been addressed, this is good to go, except that we might want to add new verbs for SetupSequence such that the default response for an exhausted sequence can be defined.
|
|
||
| public override void Execute(ICallContext call) | ||
| { | ||
| base.Execute(call); |
There was a problem hiding this comment.
Done. Added a unit test that ensures Verify can correctly check a sequence's invocation count.
| { | ||
| // we get here if there are more invocations than configured responses. | ||
| // if the setup method does not have a return value, we don't need to do anything; | ||
| // if it does have a return value, we produce the default value. |
There was a problem hiding this comment.
The new implementation acts differently; see explanation in #467 (comment). I personally favour the new behaviour as it brings SetupSequence more in line with how other Setup methods work: unless a setup is a conditional setup (.When(...)), then it overrides any previous setup for the same invocation.
SetupSequence to make it thread-safe (Draft)SetupSequence to make it thread-safe
3e5564e to
6bedd9f
Compare
6bedd9f to
9139dfc
Compare
9139dfc to
c87cdcf
Compare
This is an early draft and shouldn't be merged just yet.It does appear to resolve #467, however since it's a complete reimplementation ofSetupSequencewhich works in a completely different way than the previous implementation, it needs much more thorough testing of edge cases, as well as a code cleanup.This resolves #467.