使用接口对后台线程进行单元测试
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);
}
}
我已经创建了一个 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);
}
}