目前最主流的单元测试框架是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提供了相当完备的功能供单元测试,但是在微服务架构中,它太重了。在一个典型的服务中,它的调用是这样的:

idea spring boot 1.x junit单元测试_junit

 

   为了测试A类,必须把B-E类全部服务都构建好,如果其中有其它微服务提供的接口,则不得不依赖挡板或集成测试环境,这样测试成本就会很高。所以,更好的做法是为B、C做mock类(对于每个被测类来说,Mock是类级别的,跟分支数无关),为B、C类的方法做stub(stub是根据B/C类对应方法有多少不同出入参对来决定的,一一对应。注:单元测试几乎所有被测类依赖的有状态类都需要Mock,接口测试则只需要Mock其他微服务的接口),这样就可以不用依赖spring环境完成测试。如下:

  idea spring boot 1.x junit单元测试_junit_02

  但是事情通常要比这更复杂,有些非业务服务类可能需要依赖spring的配置信息,有一些利用了spring ioc的各种特性比如ApplicationContext.getBean()、多数据源切换、AOP拦截器、复杂逻辑生成文件等,对于这些特殊场景,仍然是需要仔细设计单元测试。正常情况下单元测试仅仅是为了测试逻辑,通常不包括事务,否则清理和准备通常也要花费不低的成本。如果一定要测试数据库,在单元测试上方法上增加@Transactional(加上会使得最后所有事务被回滚)注解反而不一定合适了,因为既然要测试数据库,则起码ACID应该测试。

  其次,对于桩而言,通常都是为了返回某个结果,对于一些包含很多字段的pojo和List,每次造这些数据也是比较耗时的,因此建议在json文件中维护相关的dto和pojo实例化数据及其配套工具类,进行统一的注入,这样单元测试的重复代码就可以大大减少。

       对于一个方法来说,单元测试的最低要求是100%的代码覆盖率,至少已知的通过、不通过、抛出的Exception得测到。除了简单的业务查询外,几乎不可能只有一个test case,如果一个方法只有一个单元测试,几乎可以肯定单元测试是为了应付,所以对每个方法,在javadoc上维护好场景清单,至少应包括:场景说明,入参(线程上下文变量),返回值/XXXException很重要,只有这样单测才会有效果。

4、要实践好单元测试,首先得掌握事半功倍的技巧,不然很容易事倍功半。