I'm not aware of a published implementation of this technique, but it's a fairly standard way of using promises. An interesting side effect of the composability, reusability, and extensibility of functional programming is that often you don't find things in libraries that you would expect.
In a less composable paradigm, you would need support in a library to do something like your throttle function, because you have to weave support for it throughout other functions. In functional code, programmers just write it themselves once, then they can reuse it everywhere.
After you write something like this, who do you share it with, and how? It's too small to be its own library. It's a little too specific to not be considered bloat in a promise library. Perhaps some sort of promise utilities library, but then it would be in a not very cohesive module with other functions vaguely related to promises, which makes it hard to find.
What happens to me a lot is I search for 10 minutes or so, then just write it myself because it would only take me a half hour. I put it in a "utilities" file I keep around with other odds and ends functions that are short but highly reusable, but don't really fit anywhere. Then six months later I happen upon it by chance in a community-maintained library with some weird name.
My point is, with functional programming, not being able to find an existing implementation for something that feels fairly "standard" is not a sign that other people are either doing it in a superior way, or that you're the first to come up with it. It's just a symptom of the difficulty of sharing short, reusable code in a way that's easy to discover by a search.
Ok - I am not sure whether the following will be of any help to you, because I made some assumptions in developing a solution which may or may not be true in your case. Maybe my "solution" is too theoretical and only works for artifical examples - I have not done any testing beyond the stuff below.
In addition, I would see the following more a workaround than a real solution but considering the lack of responses I think it might still be better than nothing (I kept watching your question waiting for a solution, but not seeing one getting posted I started playing around with the issue).
But enough said: Let's say we have a simple data service which can be used to retrieve an integer:
public interface IDataService
{
Task<int> LoadMagicInteger();
}
A simple implementation uses asynchronous code:
public sealed class CustomDataService
: IDataService
{
public async Task<int> LoadMagicInteger()
{
Console.WriteLine("LoadMagicInteger - 1");
await Task.Delay(100);
Console.WriteLine("LoadMagicInteger - 2");
var result = 42;
Console.WriteLine("LoadMagicInteger - 3");
await Task.Delay(100);
Console.WriteLine("LoadMagicInteger - 4");
return result;
}
}
Now, a problem arises, if we are using the code "incorrectly" as illustrated by this class. Foo
incorrectly accesses Task.Result
instead of await
ing the result like Bar
does:
public sealed class ClassToTest
{
private readonly IDataService _dataService;
public ClassToTest(IDataService dataService)
{
this._dataService = dataService;
}
public async Task<int> Foo()
{
var result = this._dataService.LoadMagicInteger().Result;
return result;
}
public async Task<int> Bar()
{
var result = await this._dataService.LoadMagicInteger();
return result;
}
}
What we (you) now need is a way to write a test which succeeds when calling Bar
but fails when calling Foo
(at least if I understood the question correctly ;-) ).
I'll let the code speak; here's what I came up with (using Visual Studio tests, but it should work using NUnit, too):
DataServiceMock
utilizes TaskCompletionSource<T>
. This allows us to set the result at a defined point in the test run which leads to the following test. Note that we are using a delegate to pass back the TaskCompletionSource back into the test. You might also put this into the Initialize method of the test and use properties.
TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;
Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());
tcs.TrySetResult(42);
var result = task.Result;
Assert.AreEqual(42, result);
this._end = true;
What's happening here is that we first verify that we can leave the method without blocking (this would not work if someone accessed Task.Result
- in this case we would run into a timeout as the result of the task is not made available until after the method has returned).
Then, we set the result (now the method can execute) and we verify the result (inside a unit test we can access Task.Result as we actually want the blocking to occur).
Complete test class - BarTest
succeeds and FooTest
fails as desired.
[TestClass]
public class UnitTest1
{
private DataServiceMock _dataService;
private ClassToTest _instance;
private bool _end;
[TestInitialize]
public void Initialize()
{
this._dataService = new DataServiceMock();
this._instance = new ClassToTest(this._dataService);
this._end = false;
}
[TestCleanup]
public void Cleanup()
{
Assert.IsTrue(this._end);
}
[TestMethod]
public void FooTest()
{
TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;
Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());
tcs.TrySetResult(42);
var result = task.Result;
Assert.AreEqual(42, result);
this._end = true;
}
[TestMethod]
public void BarTest()
{
TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;
Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Bar());
tcs.TrySetResult(42);
var result = task.Result;
Assert.AreEqual(42, result);
this._end = true;
}
}
And a little helper class to test for deadlocks / timeouts:
public static class TaskTestHelper
{
public static void AssertDoesNotBlock(Action action, int timeout = 1000)
{
var timeoutTask = Task.Delay(timeout);
var task = Task.Factory.StartNew(action);
Task.WaitAny(timeoutTask, task);
Assert.IsTrue(task.IsCompleted);
}
}
Best Answer
This
async/await
code:Would be basically equivalent to this:
For complete equivalence, including synchronous exceptions in any of the functions calls, it would be something like this: