Search code examples
c#unit-testingasynchronoustpl-dataflowbufferblock

TPL Unit Test BufferBlock LinkTo TargetBlock


I am trying to create unit tests for a TPL BufferBlock and want to test that an exception gets thrown. However, the test passes before the exception gets thrown.

EDIT

Also, this is a long running process so I do not call complete. This process runs until the app is closed

Here's the code:

public class PriorityMessageQueue 
{
        private BufferBlock<MyMessage> _messageBufferBlock;
        private async Task<bool> Init()
        {
            var executionDataflowBlockOptions = new ExecutionDataflowBlockOptions
            {
                MaxDegreeOfParallelism = Environment.ProcessorCount,
                BoundedCapacity = 1000
            };

            var prioritizeMessageBlock = new ActionBlock<MyMessage>(msg =>
            {
                try
                {
                    SetMessagePriority(msg);
                }
                catch (Exception)
                {
                    throw;
                }

            });

            _messageBufferBlock = new BufferBlock<MyMessage>(executionDataflowBlockOptions);
            _messageBufferBlock.LinkTo(prioritizeMessageBlock);
        }

        public async Task<bool> EnqueueAsync(MyMessage message)
        {
            if (message == null)
            {
                throw new ArgumentNullException(nameof(message), "The  message object is NULL. Cannot enqueue a NULL object.");
            }

            return await _messageBufferBlock.SendAsync(message);
        }

        private void SetMessagePriority(MyMessage message)
        {
           if (message.MessageType.Equals(MessageType.Unknown))
           {
               throw new ArgumentException("The SCBA Message Type is 'Unknown'. Cannot set the Message Priority on an 'Unknown' message type.");
           }

           switch (message.MessageType)
           {
               case MessageType.Admin:                   
                   message.MessagePriority = MessagePriority.Admin;
                   break;
               case MessageType.AuthUser:
                   message.MessagePriority = MessagePriority.High;
                   break;                
               case MessageType.UnknownUser:
                   message.MessagePriority = MessagePriority.Normal;
                   break;                
               default:
                   message.MessagePriority = MessagePriority.Normal;
                   break;
           }
        }

}

Here's the test code

    [TestClass()]
    public class PriorityMessageQueueTests
    {
        private IPriorityMessageQueue _queue;

        [TestInitialize]
        public void Init()
        {
            IUnityContainer container = new UnityContainer();

            var logRepository = new Mock<ILogRepository>();

            container.RegisterInstance(logRepository.Object);

            _queue = new PriorityMessageQueue(logRepository.Object);
        }

        [TestCleanup]
        public void CleanUp()
        {
            _queue.Dispose();
        }

        [TestMethod()]
        [ExpectedException(typeof(ArgumentNullException))]
        public async Task EnqueueTest_NullMessage()
        {
            await _queue.EnqueueAsync(null);
        }

        [TestMethod()]
        public async Task EnqueueTest_UnknownMessageType()
        {
            var message = new MyMessage
            {
                Id = 1,
                Text = "Unit Test"
            };

            var result = await _queue.EnqueueAsync(message);

            Assert.AreEqual(true, result);
        }

        [TestMethod()]
        public void DequeueTest()
        {
            Assert.Fail();
        }

        [TestMethod()]
        public void GetNextInQueue()
        {
            Assert.Fail();
        }
    }

The ArgumentException exception gets thrown correctly in SetMessagePriority because the 'MessageType' equals MessageType.Unknown. However, by the time ArgumentException is thrown the unit test EnqueueTest_UnknownMessageType has passed successfully because

var result = await _queue.EnqueueAsync(message);

returns 'true' before the exception gets thrown. How do I write the test EnqueueTest_UnknownMessageType so that it fails because the exception gets thrown?

I have tried adding

[ExpectedException(typeof(ArgumentException))]

to the test, but it still passes


Solution

  • As @JSteward pointed out, the exception that you are expecting to see won't be part of the Task which you are awaiting in the EnqueueAsync() method.

    Reading through the docs for DataflowBlock.SendAsync() says the following about the return value:

    If the target accepts and consumes the offered element during the call to SendAsync, upon return from the call the resulting Task<TResult> will be completed and its Result property will return true. If the target declines the offered element during the call, upon return from the call the resulting Task<TResult> will be completed and its Result property will return false.

    The Task only conveys that the message was received or declined by the first block. There is no mention of propagating exceptions from subsequent blocks back to the original Task.

    If you are looking for an early validation of the message type, you could do that check right in your EnqueueAsync() call, removing that block from your dataflow pipeline.

    Otherwise, if you want that to be a step in a larger dataflow pipeline, you could inject a BroadcastBlock<T> to which you can attach the rest of the "happy path" pipeline to process the known message types as well as a block that only receives unknown message types where you can take whatever action you would like to communicate an unknown message type was received.