之前曾经总结过一篇文章:基于spring-boot的应用程序的单元测试方案,但是当时只是从技术实现的角度去研究单元测试,很多概念没有搞清楚。本文在重新梳理脉络,丰富概念的基础上,整合了前文的大部分内容,但是有一部分几乎在实践中用不到的内容就被删去了。
在我的个人wiki站点,可以获得更好的阅读体验喔:基于spring-boot的应用程序的单元+集成测试方案
概念解析 单元测试和集成测试测试领域有很多场景,比如单元测试,集成测试,系统测试,冒烟测试,回归测试,端到端测试,功能测试等。测试的分类方式各有不同,一些测试场景也可能存在重叠。具体这些场景的概念和区别,大家可以阅读文末给出的参考资料。
这里主要以程序员的视角谈一下我理解的单元测试和集成测试。
单元测试是编写单元测试类,针对类级别的测试。比如使用Junit框架,针对一个类,写一个测试类,测试目标类的大部分主要方法。
需要注意单元测试的级别是类。项目当中,类之间的依赖调用是很常见的事,如果你要测试一个类,而这个目标类又调用了另一个类,那么在测试时就没有遵守“在一个类范围内进行测试”,自然算不得单元测试。
如上图所示,假设A,B,C,D四个类之间存在上述的依赖关系,我们要测试类A,那么如何遵守“在类A的范围内测试”?
这就是模拟框架要解决的问题了,通过模拟B和C,我们可以在测试A的时候,调用B和C的模拟对象,而不是实际的B和C。下文会有详细介绍。
如果在测试时超脱一个类的范围,那就可以称为集成测试。如上图所示,你可以测试类A,它会直接或间接调用其他三个类,这就可以叫做集成测试。如果你去测试类C,因为它会调用D,也可以称为集成测试。
如果纯粹按照单元测试的概念,把这个工作代入到一个大型的项目,成百上千的类需要编写测试类,而且类之间的依赖需要编写模拟代码。这样的工作太过庞大,对项目来说应该是得不偿失的。
我推荐的做法是识别核心代码,或者说是重要的代码,只对这些代码做精细的单元测试。除此之外,都通过集成测试来覆盖。集成测试时优先从最顶层开始,让测试自然流下来。然后根据代码测试覆盖报告,再进行补刀。
Mock和Stub此处介绍的mock和stub,是作者基于mockito框架的理解,行业内对这两个概念的定义和此处的理解可能有所出入。作者不追求对概念有“专业的定义”或者“精确的定义”,如果读者有此追求,可另外查阅其他资料。
上文讲到,在做单元测试的时候,需要屏蔽目标类的依赖,mock和stub就是这种操作涉及到的两个概念。
在项目代码中,经常会涉及依赖多个外部资源的情况,比如数据库、微服务中的其他服务。这表示在测试的时候需要先做很多准备工作,比如准备数据库环境,比如先把依赖的服务run起来。
另外,还需要考虑消除测试的副作用,以使测试具备幂等性。比如如果测试会修改数据库,那么是否会影响二次测试的结果,或者影响整个测试环境?
对外部的资源依赖进行模拟,是一个有效的解决方案。即测试时不是真正的操作外部资源,而是通过自定义的代码进行模拟操作。我们可以对任何的依赖进行模拟,从而使测试的行为不需要任何准备工作或者不具备任何副作用。
在这个大环境下,可以解释mock和stub的含义。当我们在测试时,如果只关心某个操作是否执行过,而不关心这个操作的具体行为,这种技术称为mock。
比如我们测试的代码会执行发送邮件的操作,我们对这个操作进行mock;测试的时候我们只关心是否调用了发送邮件的操作,而不关心邮件是否确实发送出去了。
另一种情况,当我们关心操作的具体行为,或者操作的返回结果的时候,我们通过执行预设的操作来代替目标操作,或者返回预设的结果作为目标操作的返回结果。这种对操作的模拟行为称为stub(打桩)。
比如我们测试代码的异常处理机制是否正常,我们可以对某处代码进行stub,让它抛出异常。再比如我们测试的代码需要向数据库插入一条数据,我们可以对插入数据的代码进行stub,让它始终返回1,表示数据插入成功。
技术实现 单元测试 测试常规的bean当我们进行单元测试的时候,我们希望在spring容器中只实例化测试目标类的实例。