使用接口对后台线程进行单元测试

Unit testing a background thread with an interface

我已经创建了一个 class、SenderClass,它将从它的构造函数中启动和 运行 一个后台工作者。 RunWorker()、运行s 方法是一个 while(true) 循环,它将从队列中弹出元素,通过方法 SendMessage() 发送它们,然后休眠一小段时间允许将新元素添加到队列中。

问题就在这里:如何测试从队列发送元素的方法,而不将其暴露给使用 class 的人?

实施:

public class SenderClass : ISenderClass
{
    private Queue<int> _myQueue = new Queue<int>();
    private Thread _worker;

    public SenderClass()
    {
        //Create a background worker
        _worker = new Thread(RunWorker) {IsBackground = true};
        _worker.Start();
    }

    private void RunWorker() //This is the background worker's method
    {
        while (true) //Keep it running
        {
            lock (_myQueue) //No fiddling from other threads
            {
                while (_myQueue.Count != 0) //Pop elements if found
                    SendMessage(_myQueue.Dequeue()); //Send the element
            }
            Thread.Sleep(50); //Allow new elements to be inserted
        }
    }

    private void SendMessage(int element)
    {
        //This is what we want to test
    }

    public void AddToQueue(int element)
    {
        Task.Run(() => //Async method will return at ones, not slowing the caller
        {
            lock (_myQueue) //Lock queue to insert into it
            {
                _myQueue.Enqueue(element);
            }
        });
    }
}

想要的界面:

public interface ISenderClass
{
    void AddToQueue(int element);
}

测试所需接口:

public interface ISenderClass
{
    void SendMessage(int element);
    void AddToQueue(int element);
}

有一个非常简单的解决方案,说我的 class 由于 Single Responsability Principle 而创建的不正确,而我的 class 的目的不是发送消息,而是 运行 是什么送来的。

我应该拥有的是另一个 class、TransmittingClass,它通过自己的接口公开方法 SendMessage(int)。 这样我就可以测试 class,而 SenderClass 应该只通过该接口调用该方法。

但是对于当前的实施,我还有哪些其他选择?

我可以让我希望测试的所有私有方法(所有)都具有 [assembly:InternalsVisibleTo("MyTests")],但是是否存在第三个选项?

看起来 SenderClass 根本不应该执行任何发送。它应该简单地维护队列。通过执行发送的构造函数注入 Action<int>。这样你就可以将 SendMessage 移动到其他地方并随意调用它。

作为一个额外的好处,您对 SendMessage 的测试不会因队列管理而混乱。

看到您的编辑,您似乎不喜欢这种方法,而且您似乎也不喜欢 InternalsVisibleTo 方法。您可以通过单独的接口公开 SendMessage 并显式实现该接口。这样 SendMessage 仍然可以通过该接口调用,但默认情况下,如果不进行一些强制转换就无法访问它。它也不会出现在智能感知自动完成列表中。

发送消息逻辑应在单独的 class 中使用单独的接口实现。这个 class 应该把新的 class 作为依赖。您可以单独测试新的 class。

public interface IMessageQueue
{
    void AddToQueue(int element);
}

public interface IMessageSender
{
    void SendMessage(object message);
}

public class SenderClass : IMessageQueue
{
    private readonly IMessageSender _sender;
    public SenderClass(IMessageSender sender)
    {
        _sender = sender;
    }
    public void AddToQueue(int element)
    {
        /*...*/
    }

    private void SendMessage()
    {
        _sender.SendMessage(new object());
    }
}

public class DummyMessageSender : IMessageSender
{
    //you can use this in your test harness to check for the messages sent
    public Queue<object> Messages { get; private set; }

    public DummyMessageSender()
    {
        Messages = new Queue<object>();
    }
    public void SendMessage(object message)
    {
        Messages.Enqueue(message);
        //obviously you'll need to do some locking here too
    }
}

编辑

为了解决您的意见,这里有一个使用 Action<int> 的实现。这允许您在测试 class 中定义消息发送操作以模拟 SendMessage 方法,而不必担心创建另一个 class。 (就个人而言,我仍然更喜欢明确定义 classes/interfaces)。

public class SenderClass : ISenderClass
    {
        private Queue<int> _myQueue = new Queue<int>();
        private Thread _worker;
        private readonly Action<int> _senderAction;

        public SenderClass()
        {
            _worker = new Thread(RunWorker) { IsBackground = true };
            _worker.Start();
            _senderAction = DefaultMessageSendingAction;
        }

        public SenderClass(Action<int> senderAction)
        {
            //Create a background worker
            _worker = new Thread(RunWorker) { IsBackground = true };
            _worker.Start();
            _senderAction = senderAction;
        }

        private void RunWorker() //This is the background worker's method
        {
            while (true) //Keep it running
            {
                lock (_myQueue) //No fiddling from other threads
                {
                    while (_myQueue.Count != 0) //Pop elements if found
                        SendMessage(_myQueue.Dequeue()); //Send the element
                }
                Thread.Sleep(50); //Allow new elements to be inserted
            }
        }

        private void SendMessage(int element)
        {
            _senderAction(element);
        }

        private void DefaultMessageSendingAction(int item)
        {
            /* whatever happens during sending */
        }

        public void AddToQueue(int element)
        {
            Task.Run(() => //Async method will return at ones, not slowing the caller
            {
                lock (_myQueue) //Lock queue to insert into it
                {
                    _myQueue.Enqueue(element);
                }
            });
        }
    }

    public class TestClass
{
    private SenderClass _sender;
    private Queue<int> _messages;

    [TestInitialize]
    public void SetUp()
    {
        _messages = new Queue<int>();
        _sender = new SenderClass(DummyMessageSendingAction);
    }

    private void DummyMessageSendingAction(int item)
    {
        _messages.Enqueue(item);
    }

    [TestMethod]
    public void TestMethod1()
    {
        //This isn't a great test, but I think you get the idea
        int message = 42;
        _sender.AddToQueue(message);
        Thread.Sleep(100);
        CollectionAssert.Contains(_messages, 42);            
    }
}