I spent the day today writing unit tests for code I wrote yesterday. No, I’m not a big fan of TDD.
I am, however, a big fan of high (as close to 100% as practical) code coverage.
The device I’m working on is very multi-threaded. As a matter of fact, a key principle is DON’T BLOCK! Because of this, there are quite a few Task’s, lots of awaiting, and several timers that poll on a regular basis.
This makes for code that can be tricky to unit test. After all, if you’re not blocking, you’re on a different thread than the unit test thread and you need to make the unit test thread wait until the code has run its course. Here’s a couple of things I’ve found to make this easier.
First, for any timers, like System.Timers.Timer, wrap them in an interface and then inject them with IoC. Doing this, will let you raise the elapsed event whenever you want. For example, the system.timers.timer class can easily be wrapped by something like this:
public class Timer : System.Timers.Timer, ITimer
{
[InjectionConstructor]
public Timer()
{
}
public Timer(double interval)
: base(interval)
{
}
}
This can then be mocked like this:
Mock timer = new Mock();
timer.Raise(t => t.Elapsed += null, (ElapsedEventArgs)null);
Note that ElapsedEventArgs has no public constructor, so the best you can do is pass in a null, which is far from ideal.
Second, if you’re going to be using Task.Run, you’ll want something that causes those tasks to run immediately. I’ve wrapped the Task class with the following (Sorry for the bad formatting--I REALLY miss live writer plug-ins . . .):
using System;
using System.Threading;
using System.Threading.Tasks;
public class BackgroundTask : Task
{
public BackgroundTask(Action action)
: base(action)
{
}
public BackgroundTask(Action action, CancellationToken cancellationToken)
: base(action, cancellationToken)
{
}
public BackgroundTask(Action action, CancellationToken cancellationToken, TaskCreationOptions creationOptions)
: base(action, cancellationToken, creationOptions)
{
}
public BackgroundTask(Action action, TaskCreationOptions creationOptions)
: base(action, creationOptions)
{
}
public BackgroundTask(Action
: base(action, state)
{
}
public BackgroundTask(Action
: base(action, state, cancellationToken)
{
}
public BackgroundTask(Action
: base(action, state, cancellationToken, creationOptions)
{
}
public BackgroundTask(Action
: base(action, state, creationOptions)
{
}
public static bool RunTasksSynchronously { get; set; }
public static new Task Run(Action action)
{
if (RunTasksSynchronously)
{
Task task = new Task(action);
task.RunSynchronously();
return task;
}
return Task.Run(action);
}
public static new Task Run(Func function)
{
if (RunTasksSynchronously)
{
Task task = new Task(() => function.Invoke());
task.RunSynchronously();
return task;
}
return Task.Run(function);
}
public static new Task Run(Action action, CancellationToken cancellationToken)
{
if (RunTasksSynchronously)
{
Task task = new Task(action);
task.RunSynchronously();
return task;
}
return Task.Run(action, cancellationToken);
}
public static new Task Run(Func function, CancellationToken cancellationToken)
{
if (RunTasksSynchronously)
{
Task task = new Task(() => function.Invoke());
task.RunSynchronously();
return task;
}
return Task.Run(function, cancellationToken);
}
public static new Task Run(Func function)
{
if (RunTasksSynchronously)
{
Task task = new Task(function);
task.RunSynchronously();
return task;
}
return Task.Run(function);
}
public static new Task Run(Func> function, CancellationToken cancellationToken)
{
if (RunTasksSynchronously)
{
Task task = function.Invoke();
return task;
}
return Task.Run(function, cancellationToken);
}
public static new Task Run(Func function, CancellationToken cancellationToken)
{
if (RunTasksSynchronously)
{
Task task = new Task(function);
task.RunSynchronously();
return task;
}
return Task.Run(function, cancellationToken);
}
}
Note that setting RunTasksSynchronously in a non-testing environment will probably do bad things, but in testing makes all of your tasks run synchronously, which makes unit testing MUCH easier.
Now if only there were an easy way to write unit tests for race conditions!