8000 Allow `DefaultValue.Mock` to mock `Task<TMockable>` and `ValueTask<TMockable>` by stakx · Pull Request #502 · devlooped/moq · GitHub
[go: up one dir, main page]

Skip to content

Allow DefaultValue.Mock to mock Task<TMockable> and ValueTask<TMockable> - #502

Merged
stakx merged 5 commits into
devlooped:developfrom
stakx:mock-task-of-t
Nov 26, 2017
Merged

stakx merged 5 commits into
devlooped:developfrom
stakx:mock-task-of-t

Conversation

@stakx
@stakx stakx commented Oct 24, 2017 •
Copy link
Copy Markdown
Contributor

This PR enables the following scenario (shown for Task<T>, but same goes for ValueTask<T>):

var mock = new Mock<IFoo>() { DefaultValue = DefaultValue.Mock };
var property = (await mock.Object.GetBarAsync()).Property;
             // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
             // With DefaultValue.Mock, Task<T> no longer returns tasks
             // that resolve to default(T), but to a mocked T, so these are
             // now awaitable straight away.

public interface IFoo
{
    Task<IBar> GetBarAsync();
}

public interface  IBar
{
    object Property { get; }
}

This closes #171.

@stakx stakx added the draft label Oct 24, 2017
@stakx stakx left a comment •
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could still use some work to make code easier to understand (esp. MockDefaultValueProvider and MockWithOptionalWrapper.Wrapper).

Also, this PR probably breaks some of MockDefaultValueProvider's current behavior. Ensure this is what it does (as mentioned in #171 (comment)):

  1. Let the empty default value provider produce a completed Task containing default(T).
  2. If that contained default value is not null, return the produced Task.
  3. Otherwise, check if T is mockable. If so, produce another completed Task containing a new Mock() and return it.
  4. Otherwise (if T is not mockable), return the empty completed Task produced in (1).

Comment thread Source/EmptyDefaultValueProvider.cs Outdated
{
// Task<T> inherits from Task, so just return Task<bool>
return GetCompletedTaskForType(typeof(bool));
return Task.FromResult(false);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This optimization is not directly related to this PR.)

Comment thread Source/EmptyDefaultValueProvider.cs Outdated
return Task.FromResult(false);
}
else if (valueType.GetTypeInfo().IsGenericType && valueType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
else if (valueType.GetTypeInfo().IsGenericType)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This optimization is not directly related to this PR, but since one commit targets code de-duplication in MockDefaultValueProvider, it seems only logical to make the same effort in EmptyDefaultValueProvider, too.)

Comment thread Source/MockDefaultValueProvider.cs Outdated
}

object value = null;
if (taskResultType == null)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still pretty ugly. What these lines of code basically amount to is, "If I have some special handling capabilities for some special type (Task<T>), don't let the fallback value provider generate a value for it; I'll deal with it myself right away." Find a way to express this more clearly in code.

Comment thread Source/MockDefaultValueProvider.cs Outdated
}

return mock != null ? mock.Object : value;
return mock.Mock != null ? mock.Wrapper ?? mock.Mock.Object : value;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't read very clearly. Probably the same as mock.Wrapper ?? mock.Mock?.Object ?? value, but it would be better to rewrite the whole method so that no convoluted expression is required at its end.

Comment thread Source/MockWithOptionalWrapper.cs Outdated

namespace Moq
{
internal struct MockWithOptionalWrapper
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type is useful because it helps us avoid wrapping and unwrapping values in a completed Task<T>, but its semantics are still somewhat confusing, esp. the fact that the Wrapper does not wrap Mock, but Mock.Object. Perhaps rename that property.

This adds some unit tests that demonstrates missing support of the
MockDefaultValueProvider for both `Task<>` and `ValueTask<>`.
It isn't enough for `MockDefaultValueProvider` to correctly wrap a
mocked object in `Task<T>` or `ValueTask<T>`. Produced mocks also have
to be added to the owning / outer mock's `InnerMocks` dictionary in
order to be included in `mock.Verify[All]`.

The tests added in this commit show that this doesn't work yet.
The main difficulty here lies in how to avoid repeated wrapping and
unwrapping of task objects, because the mocked objects are required
both wrapped in tasks (for method return values) and unwrapped as the
plain mock object (for `InnerMocks` verification). (This is why the
helper type `MockWithWrappedMockObject` gets introduced.)
@stakx stakx changed the title Allow DefaultValue.Mock to mock Task<TMockable> Allow DefaultValue.Mock to mock Task<TMockable> and ValueTask<TMockable> Nov 26, 2017
@stakx stakx removed the draft label Nov 26, 2017
Comment thread Source/Mock.cs
}

var result = (useAlternateProvider ?? this.DefaultValueProvider).GetDefaultReturnValue(method, this);
var unwrappedResult = TryUnwrapResultFromCompletedTaskRecursively(result);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the standard default value providers (Empty and Mock), it would be great if the wrapping (inside the provider) and unwrapping (here) could be avoided. It should be possible to add a fast path as follows:

  1. Check whether this.DefaultValue != DefaultValue.Custom. If false, stop.
  2. Check whether method returns a task. If so, ask the provider not for the actual task return type via GetDefaultReturnValue, but for the type of the wrapped value via GetDefaultValue.
  3. Perform the wrapping into completed tasks here, given the value returned from the provider.

This would save us one unnecessary recursive task unwrapping operation.

@stakx
stakx merged commit d6e272e into devlooped:develop Nov 26, 2017
@stakx
stakx deleted the mock-task-of-t branch November 26, 2017 21:01
@devlooped devlooped locked and limited conversation to collaborators Sep 7, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0