1
public class Composer
{
    private Task _ComposerTask;    
    private ConcurrentQueue<IValue> _Values;   
    public bool IsConnected { get; }

    // Other dependencies
    private IClient _Client;
    private IWriter _Writer

    public Task async ConnectAsync()
    {
        this.IsConnected = await _Client.ConnectAsync();
        _ComposerTask = Task.Run(() => this.Start());
    }

    private void Start()
    {
        while(this.IsConnected)
        {
            IValue value;
            if(_Values.TryDequeue(out value) == false)
                continue;

            _Writer.Write(value);
        }
    }

    public void Send(IValue value)
    {
        _Values.Enqueue(value);
    }
}

When connected successfully Composer class execute Start method asynchronously(on another thread).
Start method check queue of values and send it forward if value exists.

My problem in testing a Send method.

[Test]
public void Send_ValidMessage_ExecuteWriteMethodWithGivenValue()
{
    // Arrange
    var fakeValue = Mock.Create<IValue>();
    var fakeWriter = Mock.Create<IWriter>();
    var fakeClient = Mock.Create<IClient>();

    Mock.Arrange(() => fakeClient.ConnectAsync().Returns(CompletedTask);

    var composer = new Composer(fakeClient, fakeWriter);

    // for (int i = 0; i < 10; i++)
    // {
    //     composer.Send(Mock.Create<IValue>());
    // }

    composer.ConnectAsync().Wait();

    // Act
    composer.Send(fakeValue);

    // Assert
    Mock.Assert(() => fakeWriter.Write(fakeValue), Occurs.Once());
}

With commented for loop test passed. But if for loop executed and inner queue will be filled with even 10 values before expected value added, then test fails with message: expected at least once, but occurs 0 times.

As I understand assertion occurs before value was queued by another thread, but how this kind of behavior can be tested?

Fabio
  • 31,528
  • 4
  • 33
  • 72

2 Answers2

0

You'll need to build some kind of "join". Something like this should suffice:

public Task JoinAsync()
{
  this.IsConnected = false;
  return _ComposerTask;
}

Note that you should also be using this in your production code. If your code doesn't eventually observe _ComposerTask, then any exceptions thrown by Start would be silently swallowed.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Sorry, I feel myself very insecure when need to add some behavior to the public API of the class only because of tests. – Fabio Sep 16 '16 at 14:43
  • @Fabio: As I noted in my answer, this code should also be used elsewhere in your system. As a general rule, all tasks should be awaited. – Stephen Cleary Sep 17 '16 at 02:01
0

My solution which I came up with is redesign Composer class or to be more specific change Send method to asynchronous:

public Task SendAsync(IValue value)
{

}

Idea behind is return Task which will completes when given value composed forward on "background" thread.

Unit test only need to await until task completed and assert proper execution.

[Test]
public async Task SendAsync_ValidMessage_ExecuteWriteMethodWithGivenValue()
{
    // Arrange
    var composer = TestFactory.GenerateComposer();

    // var tasks = new List<Task>();
    // for (int i = 0; i < 10; i++)
    // {
    //     tasks.Add(composer.SendAsync(Mock.Create<IValue>()));
    // }

    await composer.ConnectAsync();

    // Act
    await composer.SendAsync(fakeValue);

    // Assert
    Mock.Assert(() => fakeWriter.Write(fakeValue), Occurs.Once());
}

My original unit test wasn't successful even without extra values added in for loop. Test failed occasionally if it was ran multiply times. I think the reason is "unpredictable" work of thread pool.

I am still not sure how to deal with the Task which need to run during full lifetime of the instance where it was started, but this will be another question.

Fabio
  • 31,528
  • 4
  • 33
  • 72
  • 1
    Windows tests support async/await, at least in the current versions of visual studio. Just await the methods like you would in normal code. make your test method do `public async Task Send_ValidMessage_ExecuteWriteMethodWithGivenValue()` then turn your `.Wait()` calls to `await`. – Scott Chamberlain Sep 16 '16 at 14:49