目前最主流的单元测试框架是junit,其中spring boot 1.x系列主要使用junit 4,spring boot 2.x主要使用junit 5;mock类和打桩的主要框架是mockito,主要有1.x(spring boot 1.x依赖),2.x(spring boot 2.0, 2.1依赖),3.x(spring boot 2.2依赖)三个版本。
0、关于单元测试首先需要理解的是的,单元测试不能代替接口测试,前者是开发的事情,后者是开发为辅、测试为主。其目的是为了验证某个方法自身的逻辑没有问题、而没有职责验证其依赖的服务是否存在问题。因此,单元测试应该是很轻量的,甚至都不应该依赖spring环境,不需要启动servlet容器,否则就成了自动化半集成测试,所以简单的增删改查不适合作为单元测试的对象。
1、安装Junit4插件。
2、复习下junit中的注解。
@BeforeClass:针对所有测试,只执行一次,且必须为static void
@Before:初始化方法,执行当前测试类的每个测试方法前执行。
@SpringBootTest:获取启动类、加载配置,确定装载Spring Boot,如果找不到@SpringBootConfiguration启动类将运行出错;
@Test:测试方法,在这里可以测试期望异常和超时时间
@After:释放资源,执行当前测试类的每个测试方法后执行
@AfterClass:针对所有测试,只执行一次,且必须为static void
@Ignore:忽略的测试方法(只在测试类的时候生效,单独执行该测试方法无效)
@RunWith:标识为JUnit的运行环境 ,缺省值 org.junit.runner.Runner,也可以是JUnit4.class。
一个单元测试类执行顺序为:
@BeforeClass
–> @Before
–> @Test
–> @After
–> @AfterClass
每一个测试方法的调用顺序为:
@Before
–> @Test
–> @After
断言测试
断言测试也就是期望值测试,是单元测试的核心之一也就是决定测试结果的表达式,Assert对象中的断言方法:
- Assert.assertEquals 对比两个值相等
- Assert.assertNotEquals 对比两个值不相等
- Assert.assertSame 对比两个对象的引用相等
- Assert.assertArrayEquals 对比两个数组相等
- Assert.assertTrue 验证返回是否为真
- Assert.assertFlase 验证返回是否为假
- Assert.assertNull 验证null
- Assert.assertNotNull 验证非null
除了常规的测试外,JUnit还通过其它特性的测试。
超时测试
如果一个测试用例比起指定的毫秒数花费了更多的时间,那么 Junit 将自动将它标记为失败。timeout 参数和 @Test注释一起使用。现在让我们看看活动中的 @test(timeout)。
@Test(timeout = 1000) public void testTimeout() throws InterruptedException { TimeUnit.SECONDS.sleep(2); System.out.println("Complete"); }
上面测试会失败,在一秒后会抛出异常 org.junit.runners.model.TestTimedOutException: test timed out after 1000 milliseconds
异常测试
你可以测试代码是否它抛出了想要得到的异常。expected 参数和 @Test 注释一起使用。现在让我们看看活动中的 @Test(expected)。
@Test(expected = NullPointerException.class) public void testNullException() { throw new NullPointerException(); }
上面代码会测试成功。
套件测试
public class TaskOneTest { @Test public void test() { System.out.println("Task one do."); } } public class TaskTwoTest { @Test public void test() { System.out.println("Task two do."); } } public class TaskThreeTest { @Test public void test() { System.out.println("Task Three."); } } @RunWith(Suite.class) // 1. 更改测试运行方式为 Suite // 2. 将测试类传入进来 @Suite.SuiteClasses({TaskOneTest.class, TaskTwoTest.class, TaskThreeTest.class}) public class SuitTest { /** * 测试套件的入口类只是组织测试类一起进行测试,无任何测试方法, */ }
3、Spring Boot 中使用 JUnit
Spring 框架提供了一个专门的测试模块(spring-test),用于应用程序的集成测试。 在 Spring Boot 中,你可以通过spring-boot-starter-test启动器快速开启和使用它,其中包含了junit、hamcrest、mockito及asset相关类。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
// 获取启动类,加载配置,确定装载 Spring 程序的装载方法,它回去寻找 主配置启动类(被 @SpringBootApplication 注解的) @SpringBootTest // 让 JUnit 运行 Spring 的测试环境, 获得 Spring 环境的上下文的支持 @RunWith(SpringRunner.class) public class EmployeeServiceImplTest { // do }
在微服务架构中,一般来说前后端是分离的,后端一般controller层会极其弱化,或者rpc服务自动暴露为REST API接口。所以webmvc层的单元测试在设计合理的架构中是不必要的,虽然Spring Boot Test提供了相当完备的功能供单元测试,但是在微服务架构中,它太重了。在一个典型的服务中,它的调用是这样的:
为了测试A类,必须把B-E类全部服务都构建好,如果其中有其它微服务提供的接口,则不得不依赖挡板或集成测试环境,这样测试成本就会很高。所以,更好的做法是为B、C做mock类(对于每个被测类来说,Mock是类级别的,跟分支数无关),为B、C类的方法做stub(stub是根据B/C类对应方法有多少不同出入参对来决定的,一一对应。注:单元测试几乎所有被测类依赖的有状态类都需要Mock,接口测试则只需要Mock其他微服务的接口),这样就可以不用依赖spring环境完成测试。如下:
但是事情通常要比这更复杂,有些非业务服务类可能需要依赖spring的配置信息,有一些利用了spring ioc的各种特性比如ApplicationContext.getBean()、多数据源切换、AOP拦截器、复杂逻辑生成文件等,对于这些特殊场景,仍然是需要仔细设计单元测试。正常情况下单元测试仅仅是为了测试逻辑,通常不包括事务,否则清理和准备通常也要花费不低的成本。如果一定要测试数据库,在单元测试上方法上增加@Transactional(加上会使得最后所有事务被回滚)注解反而不一定合适了,因为既然要测试数据库,则起码ACID应该测试。
其次,对于桩而言,通常都是为了返回某个结果,对于一些包含很多字段的pojo和List,每次造这些数据也是比较耗时的,因此建议在json文件中维护相关的dto和pojo实例化数据及其配套工具类,进行统一的注入,这样单元测试的重复代码就可以大大减少。
对于一个方法来说,单元测试的最低要求是100%的代码覆盖率,至少已知的通过、不通过、抛出的Exception得测到。除了简单的业务查询外,几乎不可能只有一个test case,如果一个方法只有一个单元测试,几乎可以肯定单元测试是为了应付,所以对每个方法,在javadoc上维护好场景清单,至少应包括:场景说明,入参(线程上下文变量),返回值/XXXException很重要,只有这样单测才会有效果。
4、要实践好单元测试,首先得掌握事半功倍的技巧,不然很容易事倍功半。