The code in question
public void StartPlaying()
{
ThreadPool.QueueUserWorkItem(ignoredState =>
{
while (_playlist.Count > 0)
{
var audioFile = _playlist.Dequeue();
if (StartedPlaying != null)
StartedPlaying(this, new TypedAudioFileEventArgs(audioFile));
audioFile.SoundPlayer.PlaySync();
audioFile.SoundPlayer.Dispose();
if (StoppedPlaying != null)
StoppedPlaying(this, new TypedAudioFileEventArgs(audioFile));
}
});
}
and my test:
[TestMethod()]
public void StartPlayIsCalledTwice_OnlyRunningOnce()
{
int timeBetweenPlays = 0;
var target = new TypedAudioFilePlayer(timeBetweenPlays);
target.AddFile(TypedAudioFileCreator.CreateWord(1, "bl"));
target.StartedPlaying += StartedPlaying_Once;
target.StartPlaying();
target.StartPlaying();
}
private bool _once = false;
private void StartedPlaying_Once(object sender, TypedAudioFileEventArgs e)
{
if (!_once)
_once = true;
else
Assert.Fail("Should not be called more than once!");
}
I believe my unit test should fail, judging by the MSDN description of ThreadPool.QueueUserWorkItem:
Queues a method for execution. The method executes when a thread pool thread becomes available.
The default ThreadPool size is 512, so two threads should immediately be available to process the StartPlaying call. I believe my code should fail since I haven’t provided any safeguards from race conditions in which both threads can access the same resource.
What’s happening here?
Because the
StartedPlayingevent is only raised if StartPlaying is called when there are items to play._playlist.Dequeue();dequeues the file you enqueue. Therefore the second time you get towhile (_playlist.Count > 0)it will immediately fail, passing the second call toStartPlayingstraight through without raising the event.Also, as Bruno Silva points out, the thread spawned by the second call to
StartPlayingmay not have a chance to execute anything before the test exits.For what it’s worth, there are
about a millionat least 2 threading mistakes in this code also:If you want to properly unit test, you need to define preconditions, expectations, actions, and postconditions:
Preconditions: you have an initialized TypedAudioFilePlayer with one file queued:
var target = new TypedAudioFilePlayer(timeBetweenPlays);target.AddFile(TypedAudioFileCreator.CreateWord(1, "bl"));Expectations: The StartedPlaying event will be raised only once if StartPlaying is called twice
target.StartedPlaying += StartedPlaying_Once;Actions: The StartPlaying method will be called twice:
target.StartPlaying();target.StartPlaying();Postconditions: The
StartedPlayingevent was only raised once:private bool _once = false;private void StartedPlaying_Once(object sender, TypedAudioFileEventArgs e){if (!_once)_once = true;elseAssert.Fail("Should not be called more than once!");}Now, your test succeeds. That’s not good in this case, because of what I explain above. You need to get your test to a failing state by eliminating the queue bug and race condition, then work on making the test pass the right way.