Search code examples
c#mockingnunitmoqtcpclient

Unit testing a class that contains multiple continuous tasks at the background


I would like to unit test the WpaConnection class which contains multiple continuous tasks. Starting of these task is dependent on TCP connection. I would like to gather some information on what would be the best way to unit test the class and want to know how I can mock the TCP client so that ConnectAsync line is skipped.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace WpaConnection
{
        public class WpaConnection : IDisposable
        {
            private readonly IPEndPoint _wapIpEndPoint;
            private readonly TcpClient _tcpClient = new TcpClient();
            private readonly Task _connectTask;
            private bool _isExiting;

            public WpaConnection(
                IPEndPoint wapIpEndPoint,
                CancellationToken stoppingToken)
            {
                _wapIpEndPoint = wapIpEndPoint;
                _connectTask = ConnectTask(wapIpEndPoint, stoppingToken);
                _isExiting = false;
            }

            public event EventHandler<WapReadEventArgs> OnRead;

            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            public void Disconnect()
            {
                if (!_connectTask.IsCompleted)
                {
                    _isExiting = true;
                }
            }

            protected virtual void Dispose(bool disposing)
            {
                if (disposing)
                {
                    _tcpClient.Dispose();
                }
            }

            private async Task ConnectTask(IPEndPoint wapIpEndPoint, CancellationToken stoppingToken)
            {
                CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
                Task readEventAsyncTask;
                Task task1;
                Task task2;
                Task task3;

                Console.WriteLine("WpaConnection: ConnectTask started");

                try
                {
                    await _tcpClient.ConnectAsync(wapIpEndPoint.Address, wapIpEndPoint.Port);
                    Console.WriteLine("WpaConnection: ConnectTask Connected to {WapEndPoint}", wapIpEndPoint);


                    readEventAsyncTask = ReadEventTask(cancellationTokenSource.Token);
                    task1 = Task1(cancellationTokenSource.Token);
                    task2 = Task2(cancellationTokenSource.Token);
                    task3 = Task3(cancellationTokenSource.Token);

                    while (!stoppingToken.IsCancellationRequested && _tcpClient.Connected && !_isExiting)
                    {
                        await Task.Delay(1, stoppingToken);
                    }

                    _tcpClient.Close();

                    Console.WriteLine("WpaConnection: ConnectTask closed TcpClient");
                }
                catch (Exception)
                {
                    _isExiting = true;
                }

                cancellationTokenSource.Cancel();


                Console.WriteLine("WpaConnection: ConnectTask exiting...");
            }

            private async Task Task3(CancellationToken stoppingToken)
            {
                Console.WriteLine("WpaConnection: Task3 started");

                try
                {
                    int sequence = 0;
                    while (!stoppingToken.IsCancellationRequested && !_isExiting)
                    {
                        Console.WriteLine($"Task3 {sequence++}");

                        await Task.Delay(1000, stoppingToken);
                    }
                }
                catch (Exception)
                {
                    _isExiting = true;
                }

                Console.WriteLine("WpaConnection: Task3 exiting...");
            }

            private async Task Task1(CancellationToken stoppingToken)
            {
                Console.WriteLine("WpaConnection: Task1 started");

                try
                {
                    int sequence = 0;
                    while (!stoppingToken.IsCancellationRequested && !_isExiting)
                    {
                        Console.WriteLine($"Task1 {sequence++}");

                        await Task.Delay(1000, stoppingToken);
                    }
                }
                catch (Exception)
                {
                    _isExiting = true;
                }

                Console.WriteLine("WpaConnection: Task1 exiting...");
            }

            private async Task Task2(CancellationToken stoppingToken)
            {
                Console.WriteLine("WpaConnection: Task2 started");

                try
                {
                    int sequence = 0;
                    while (!stoppingToken.IsCancellationRequested && !_isExiting)
                    {
                        Console.WriteLine($"Task2 {sequence++}");

                        await Task.Delay(1000, stoppingToken);
                    }
                }
                catch (Exception)
                {
                    _isExiting = true;
                }

                Console.WriteLine("WpaConnection: Task2 exiting...");
            }

            private async Task ReadEventTask(CancellationToken stoppingToken)
            {
                Console.WriteLine("WpaConnection: ReadEventTask started");
                byte[] buffer = new byte[512];

                try
                {
                    while (!stoppingToken.IsCancellationRequested && !_isExiting && _tcpClient.Connected)
                    {
                        int length = await _tcpClient.GetStream().ReadAsync(buffer, 0, buffer.Length, stoppingToken);
                        if (length > 0)
                        {
                            byte[] data = new byte[length];
                            Buffer.BlockCopy(buffer, 0, data, 0, length);

                            OnRead?.Invoke(
                                this,
                                new WapReadEventArgs()
                                {
                                    WapEndPoint = _wapIpEndPoint,
                                    Length = length,
                                    Buffer = data,
                                });
                        }
                    }
                }
                catch (Exception)
                {
                    _isExiting = true;
                }

                Console.WriteLine("WpaConnection: ReadEventTask exiting...");
            }
        }

        public class WapReadEventArgs : EventArgs
        {
            public IPEndPoint WapEndPoint { get; set; }

            public int Length { get; set; }

            public byte[] Buffer { get; set; }
        }

    internal class Program
    {
        static void Main(string[] args)
        {
            IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Any, 1234);
            CancellationTokenSource cancellationToken = new CancellationTokenSource();
            WpaConnection connection = new WpaConnection(iPEndPoint, cancellationToken.Token);

            Task.Delay(1000).Wait();

            cancellationToken.Cancel();

            Console.ReadLine();
        }
    }
}

Learning the best way of testing such classes.


Solution

  • My advice would be to create an actual server to test against. This might be an internal copy of the "real" server if you can host an internal instance. Or a separate implementation if this is an external API where a test instance is infeasible.

    This ensures that you are testing the critical part of the implementation, how the actual communication is done. If you replace the TCPClient with a mock, what are you actually testing? that you can copy bytes from a stream to an array? If so, move that to a helper method that is easier to test.

    As an example, I do not see any message framing, and this could easily cause issues that could be missed unless you can write a mock that is a faithful representation of how TCP actually works.

    Tests like this that take a longer time to run may need to be categorized differently from tests intended to complete within a few milliseconds. You could put integration tests into a separate project, or use some attribute to provide categorization. You can also chose if the "server" is a persistent, company wide, instance. Or a local instance started and stopped by the tests themselves.

    If you for some reason find integration testing infeasible I would consider if it is cost effective to write automated tests. Things like UIs and and integrations tend to be more costly to write and maintain unit tests for. And the whole idea of automated tests is that the value they provide should exceed the cost.