Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Easier async setups through a new Await(...) operator #1007

Closed
stakx opened this issue Apr 25, 2020 · 2 comments · Fixed by #1126
Closed

Easier async setups through a new Await(...) operator #1007

stakx opened this issue Apr 25, 2020 · 2 comments · Fixed by #1126
Assignees
Milestone

Comments

@stakx
Copy link
Contributor

stakx commented Apr 25, 2020

The discussion below assumes the definition of the following type IX and mock parentMock:

public interface IX
{
    IX Child { get; }
    Task<IX> GetChildAsync();

    string Name { get; }
    ValueTask<string> GetNameAsync();
}

var parentMock = new Mock<IX>();

Problem:

Today (as of version 4.14.0), Moq has the ability to transparently set up whole object graphs through fluent setup expressions, such as this one:

parentMock.Setup(p => p.Child.Name)
          .Returns("Alice");

But Moq doesn't offer anything comparable when async methods come into play.

Say you would like to do the same kind of setup, but instead of using the synchronous property .Child you want to use the .GetChildAsync() method. The most succinct solution currently possible is perhaps this:

parentMock.Setup(p => p.GetChildAsync())
          .ReturnsAsync(Mock.Of<IX>(c => c.Name == "Alice"));

which makes use of ReturnsAsync, which—like other ...Async extension methods—tries to make async method setups a little easier. Without those helpers, the solution would be even more elaborate:

parentMock.Setup(p => p.GetChildAsync())
          .Returns(() => Task.FromResult(Mock.Of<IX>(c => c.Name == "Alice")));

Even today, we're still trying to make async methods easier to setup (e.g. by adding more ...Async helper methods, or in #384), but the current approaches don't do anything about async support inside fluent setup expressions. Can something be done about that?

Proposed solution:

Say you would like to do the same kind of setup, but instead of using the synchronous property .Child you want to use the .GetChildAsync() method.

I think the ideal solution would be this:

parentMock.Setup(async p => (await p.GetChildAsync()).Name)
          .Returns("Alice");

Unfortunately, the C# compiler does not allow await in LINQ expression trees.

BUT we could compensate this lack of compiler support with a static helper method Await, which would be used as follows:

parentMock.Setup(p => Await(p.GetChildAsync()).Name)
          .Returns("Alice");

which happens to be even shorter than using native C# keywords.

I strongly suspect that this is feasible, and that it would make all existing ...Async helper methods redundant. Take ReturnsAsync for example:

parentMock.Setup(p => p.GetNameAsync()).ReturnsAsync("Alice");

could be rewritten as:

parentMock.Setup(p => Await(p.GetNameAsync())).Returns("Alice");

or ThrowsAsync:

parentMock.Setup(p => p.GetChildAsync()).ThrowsAsync(...);

could be rewritten as:

parentMock.Setup(p => Await(p.GetChildAsync())).Throws(...);

Something that's a little less clear is Callback:

parentMock.Setup(p => Await(p.GetChildAsync())).Callback(...);

This setup expression suggests that the callback should only execute on an await parentMock.Object.GetChildAsync(), but not on a simple execution of parentMock.Object.GetChildAsync() (i.e. without the await). That is, Callback may have to be merged with any Returns present; also, a Callback after Returns might have to be transformed to a task continuation (resultTask.ContinueWith(...)). Not sure how that should go.

I'll shortly begin prototyping this. Comments, questions, and suggestions are welcome!

@stakx stakx self-assigned this Apr 25, 2020
@stakx stakx changed the title Better support for async method through a new Await(...) operator Easier async setups through a new Await(...) operator Apr 25, 2020
@stakx stakx pinned this issue Apr 25, 2020
@stakx stakx unpinned this issue Apr 28, 2020
@stakx

This comment has been minimized.

@stakx
Copy link
Contributor Author

stakx commented Jan 1, 2021

This feature has essentially made it into the code base; but for now, only task.Result instead of Await(anything) is supported. "Await anything" support may be added in the future.

@stakx stakx added this to the 4.16.0 milestone Jan 1, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment