测试什么
每个单元测试策略的总体目标是:
- 完整的代码覆盖率:这意味着您的应用程序的每一行代码在完整的测试套件中至少执行一次。
- 规范的全面覆盖:通常,软件开发从功能规范开始,该规范准确地说明了软件的功能以及在每种可能的情况下它如何反应。规范中提到的每个要求都需要至少一个测试。当您没有规范时,最少测试的指南可能是HTTP 协议本身的规范。
每当您测试服务器应用程序时,您必须记住,您不能假设您的客户端将是 HTTP 协议的完美实现。在现实世界中,您的服务器将面临错误地实现它或具有不可靠连接的客户端,这些客户端在您的应用程序的任何状态下都会突然终止。这甚至还没有考虑到故意尝试在您的服务器中查找和利用漏洞的恶意客户端。这些也是您需要测试用例的情况。
服务器通常是多线程的。这意味着应该测试多线程带来的常见问题(如竞争条件、死锁、同步问题、繁忙的自旋等)。不幸的是,这些问题很难预测,甚至更难编写有意义的单元测试。不仅因为这些问题通常通过不同组件的交互表现出来,这是集成和系统测试的范围,而不是单元测试。
如何测试
单元测试通常不应该依赖于外部资源,因为当它们这样做时,您是在测试该资源,而不是您自己的应用程序。当您的应用程序的功能严重依赖外部系统时,例如 http 服务器的情况,应该模拟其他系统。
这是通过使用模拟对象实现外部资源来完成的。模拟对象是代替依赖于外部资源的对象(通过扩展它或通过实现相同的接口)的对象,但仅模拟其行为。在网络服务器的上下文中,您将创建一个扩展Socket的类,但使用模拟外部客户端而不实际访问网络的东西覆盖 Socket 的所有方法。
然后,您可以在单元测试期间使用它来测试您的类,而不是普通的套接字。
这使您可以轻松实现特殊的模拟套接字来测试特殊的测试条件,例如响应缓慢、非常快或只是错误的客户端。
new Socket()
您可能想知道“但是当我在我想要测试的类中创建一个内部时我应该怎么做呢”?答案是,为了使应用程序可单元测试,您不应该这样做。关键字是依赖注入。简而言之,依赖注入意味着一个类不应该使用new
关键字,除非它是该类的工厂或生成器。一个类使用的任何对象都不应该由它创建。它们应该通过构造函数或 setter 方法提供给它。
例子:
因此,当您有一个ConnectionListener
使用 aServerSocket
等待客户端连接然后对它们执行某些操作的类时,您不应该在其构造函数中创建该 ServerSocket。相反,您应该将 ServerSocket 传递给 ConnectionListener 的构造函数。在生产代码中,这将是一个普通的服务器套接字。但是在您的单元测试中,您可以传递一个扩展 ServerSocket 的模拟对象。这个模拟对象将模拟一个或多个客户端,然后向单元测试框架报告 ConnectionListener 的行为是否符合预期。