Reputation: 3432
I'm trying to implement fire and forget functionality, using the Task Parallel Library. With an inline call to Task.Factory.StartNew
, everything works as expected. However, I want to move the Task.Factory.StartNew
call into a separate class so that I can add logging, error handling, etc, and potentially upgrade the code in the future as better threading classes, etc are added to the .NET Framework, without duplicating code.
Below is a unit test that I would expect to pass, but that does not. I would appreciate help trying to figure out how to make this work.
[TestFixture]
public class ThreadingServiceFixture
{
public static bool methodFired = false;
[Test]
public void CanFireAndForgetWithThreadingService()
{
try
{
var service = new ThreadingService();
service.FireAndForget(() => methodFired = true);
var endTime = DateTime.Now.AddSeconds(1);
while(DateTime.Now < endTime)
{
//wait
}
Assert.IsTrue(methodFired == true);
}
finally
{
methodFired = false;
}
}
}
public class ThreadingService
{
public Task FireAndForget(Action action)
{
return Task.Factory.StartNew(() => action);
}
}
Upvotes: 2
Views: 801
Reputation: 35905
You did not invoke the action in the ThreadingService
The code should read something like
public class ThreadingService
{
public Task FireAndForget(Action action)
{
return Task.Factory.StartNew(() => action.Invoke());
}
}
Additional note: testing state with a public field is evil. Think about repeatability, maintenance, running tests in different order. You should move bool methodFired
inside the test. I would also assume there is a better technique to test this (but I am not sure which one).
Upvotes: 2
Reputation: 8206
Testing threaded code is hard. Basing your tests on timing is a bad idea, they may become non-deterministic and you might observe erratic behavior on you build server. Imagine a tests that sometime passes and sometimes doesn't!
Your code has a bug, since you are not actually invoking the action.
But consider this variation:
[Test]
[TimeOut(5000)]
public void CanFireAndForgetWithThreadingService()
{
var service = new ThreadingService();
ManualResetEvent mre = new ManualRestEvent(bool); // I never remember what is the default...
service.FireAndForget(() => mre.Set() /*will release the test asynchroneously*/);
mre.WaitOne(); // blocks, will timeout if FireAndForget does not fire the action.
}
Yes, we are still using timing. But the test the timeout will happen only if the code breaks! In all other scenarios, the test is absolutely predictable and takes a very short amount of time to execute, no waiting and praying for timing issues not to happen ;-)
Upvotes: 1
Reputation: 9497
If is "fire and forget" you don't need to return the Task
from the FireAndForget
method, because the caller could get that Task
and cancel it (strictly speaking the caller would "remember" of the call).
If you want to invoke this method from many services that do not inherit from a common ThreadingService
you can implement an extension method via an interface.
public interface IFireAndForget
{
// no member needed.
}
public static class FireAndForgetExtensions
{
public static void FireAndForget(this IFireAndForget obj, Action action)
{
// pass the action, not a new lambda
Task.Factory.StartNew(action);
}
}
// using
public class ThreadingService : IFireAndForget
{
}
Also note the in your method you have to pass the action
to the StartNew
method insted of pass a lambda that return the action
parameter.
Upvotes: 2
Reputation: 24383
You're not executing the action, you're just returning it.
Try:
return Task.Factory.StartNew(() => action());
Upvotes: 3