Spring Boot Junit 单元测试

JUnit 是一个回归测试框架,被开发者用于实施对应用程序的单元测试,加快程序编制速度,同时提高编码的质量。

JUnit 测试框架具有以下重要特性:

  • 测试工具
  • 测试套件
  • 测试运行器
  • 测试分类

了解 Junit 基础方法

加入依赖

在 pom.xml 中加入依赖:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
    <version>4.12</version>
</dependency>

创建测试类和测试方法

  1. 测试类的的命名规则一般是 xxxTest.java ;
  2. 测试类中测试的方法可以有前缀,这个看统一标准,所以有时候会发现别人的测试方法上有test前缀;
  3. 并且测试方法上加上注解 @Test

使用 IDEA 中,选中当前类名,使用快捷键 ALT + ENTER(WIN),向下选则 Create Test 回车,即可进入生成测试类的选项中,再次回车,就快速的生成测试类。

OK 完你会发现,生成的测试类在 src/test 目录下,测试类和源代码的包名 是一致的。生成后结果(注意下生成的方法名是不加 test):

public class HelloServiceImplTest {

    @Before
    public void setUp() throws Exception {
    }

    @After
    public void tearDown() throws Exception {
    }

    @Test
    public void say() {
    }
}

JUnit中的注解

  • @BeforeClass:针对所有测试,只执行一次,且必须为static void
  • @Before:初始化方法,执行当前测试类的每个测试方法前执行。
  • @Test:测试方法,在这里可以测试期望异常和超时时间
  • @After:释放资源,执行当前测试类的每个测试方法后执行
  • @AfterClass:针对所有测试,只执行一次,且必须为static void
  • @Ignore:忽略的测试方法(只在测试类的时候生效,单独执行该测试方法无效)
  • @RunWith:可以更改测试运行器 ,缺省值 org.junit.runner.Runner

一个单元测试类执行顺序为:

@BeforeClass –> @Before –> @Test –> @After –> @AfterClass

每一个测试方法的调用顺序为:

@Before –> @Test –> @After

超时测试

如果一个测试用例比起指定的毫秒数花费了更多的时间,那么 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 {
    /**
     * 测试套件的入口类只是组织测试类一起进行测试,无任何测试方法,
     */
}

参数化测试

Junit 4 引入了一个新的功能参数化测试。参数化测试允许开发人员使用不同的值反复运行同一个测试。你将遵循 5 个步骤来创建参数化测试。

  • 用 @RunWith(Parameterized.class)来注释 test 类。
  • 创建一个由 @Parameters 注释的公共的静态方法,它返回**一个对象的集合(数组)**来作为测试数据集合。
  • 创建一个公共的构造函数,它接受和一行测试数据相等同的东西。
  • 为每一列测试数据创建一个实例变量。
  • 用实例变量作为测试数据的来源来创建你的测试用例。
//1.更改默认的测试运行器为RunWith(Parameterized.class)
@RunWith(Parameterized.class)
public class ParameterTest {
    // 2.声明变量存放预期值和测试数据
    private String firstName;
    private String lastName;

    //3.声明一个返回值 为Collection的公共静态方法,并使用@Parameters进行修饰
    @Parameterized.Parameters //
    public static List<Object[]> param() {
        // 这里我给出两个测试用例
        return Arrays.asList(new Object[][]{{"Mike", "Black"}, {"Cilcln", "Smith"}});
    }

    //4.为测试类声明一个带有参数的公共构造函数,并在其中为之声明变量赋值
    public ParameterTest(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    // 5. 进行测试,发现它会将所有的测试用例测试一遍
    @Test
    public void test() {
        String name = firstName + " " + lastName;
        assertThat("Mike Black", is(name));
    }
}

Hamcrest

JUnit 4.4 结合 Hamcrest 提供了一个全新的断言语法——assertThat。

语法:

assertThat( [actual], [matcher expected] );

assertThat 使用了 Hamcrest 的 Matcher 匹配符,用户可以使用匹配符规定的匹配准则精确的指定一些想设定满足的条件,具有很强的易读性,而且使用起来更加灵活。

具体使用的一些匹配规则可以查看源码。

Spring Boot 中使用 JUnit

Spring 框架提供了一个专门的测试模块(spring-test),用于应用程序的集成测试。 在 Spring Boot 中,你可以通过spring-boot-starter-test启动器快速开启和使用它。

加入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Spring Boot 测试

// 获取启动类,加载配置,确定装载 Spring 程序的装载方法,它回去寻找 主配置启动类(被 @SpringBootApplication 注解的)
@SpringBootTest
// 让 JUnit 运行 Spring 的测试环境, 获得 Spring 环境的上下文的支持
@RunWith(SpringRunner.class)
public class EmployeeServiceImplTest {
    // do 
}

Spring MVC 测试

当你想对 Spring MVC 控制器编写单元测试代码时,可以使用@WebMvcTest注解。它提供了自配置的 MockMvc,可以不需要完整启动 HTTP 服务器就可以快速测试 MVC 控制器

1、需要测试的 Controller:

@RestController
@RequestMapping(value = "/emp", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class EmployeeController {
    private final EmployeeService employeeService;

    @Autowired
    public EmployeeController(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @GetMapping
    public ResponseEntity<List<EmployeeResult>> listAll() {
        return ResponseEntity.ok(employeeService.findEmployee());
    }
}

2、编写 MockMvc 的测试类:

@RunWith(SpringRunner.class)
@WebMvcTest(EmployeeController.class)
public class EmployeeController2Test {
    @Autowired
    private MockMvc mvc;

    @MockBean
    private EmployeeService employeeService;

    public void setUp() {
        // 数据打桩,设置该方法返回的 body一直 是空的
        Mockito.when(employeeService.findEmployee()).thenReturn(new ArrayList<>());
    }

    @Test
    public void listAll() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/emp"))
                .andExpect(status().isOk()) // 期待返回状态吗码200
                // JsonPath expression  https://github.com/jayway/JsonPath
                //.andExpect(jsonPath("$[1].name").exists()) // 这里是期待返回值是数组,并且第二个值的 name 存在,所以这里测试是失败的
                .andDo(print()); // 打印返回的 http response 信息
    }
}

使用@WebMvcTest注解时,只有一部分的 Bean 能够被扫描得到,它们分别是:

  • @Controller
  • @ControllerAdvice
  • @JsonComponent
  • Filter
  • WebMvcConfigurer
  • HandlerMethodArgumentResolver
  • 其他常规的@Component(包括@Service、@Repository等)Bean 则不会被加载到 Spring 测试环境上下文中。
  • 所以我在上面使用了数据打桩,Mockito 在这篇文章最后一节。

3、我们也可以注入Spring 上下文的环境到 MockMvc 中,如下编写 MockMvc 的测试类:

@RunWith(SpringRunner.class)
@SpringBootTest
public class EmployeeControllerTest {
    /**
     * Interface to provide configuration for a web application.
     */
    @Autowired
    private WebApplicationContext ctx;

    private MockMvc mockMvc;

    /**
     * 初始化 MVC 的环境
     */
    @Before
    public void before() {
        mockMvc = MockMvcBuilders.webAppContextSetup(ctx).build();
    }

    @Test
    public void listAll() throws Exception {
        mockMvc
                .perform(get("/emp") // 测试的相对地址
                .accept(MediaType.APPLICATION_JSON_UTF8) // accept response content type

值得注意的是需要首先使用 WebApplicationContext 构建 MockMvc

Spring Boot Web 测试

当你想启动一个完整的 HTTP 服务器对 Spring Boot 的 Web 应用编写测试代码时,可以使用@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)注解开启一个随机的可用端口。Spring Boot 针对 REST 调用的测试提供了一个 TestRestTemplate 模板,它可以解析链接服务器的相对地址。

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class EmployeeController1Test {
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void listAll() {
        ResponseEntity<List> result = restTemplate.getForEntity("/emp", List.class);
        Assert.assertThat(result.getBody(), Matchers.notNullValue());
    }
}

其实之前上面的测试返回结果不是很正确,只能接收个List,给测试代码添加了不少麻烦,还好最终找到了解决办法:

 @Test
    public void listAll() {
        // 由于我返回的是 List 类型的,一直想不到办法解决,网上给出了解决办法,使用 exchange 函数代替
        //public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
        //            HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType,
        //            Object... urlVariables) throws RestClientException {
        ParameterizedTypeReference<List<EmployeeResult>> type = new ParameterizedTypeReference<List<EmployeeResult>>() {};
        ResponseEntity<List<EmployeeResult>> result = restTemplate.exchange("/emp", HttpMethod.GET, null, type);
        Assert.assertThat(result.getBody().get(0).getName(), Matchers.notNullValue());
    }

Spring Data JPA 测试

我们可以使用 @DataJpaTest注解表示只对 JPA 测试;@DataJpaTest注解它只扫描@EntityBean 和装配 Spring Data JPA 存储库,其他常规的@Component(包括@Service、@Repository等)Bean 则不会被加载到 Spring 测试环境上下文。

@DataJpaTest 还提供两种测试方式:

  1. 使用内存数据库 h2database,Spring Data Jpa 测试默认采取的是这种方式;
  2. 使用真实环境的数据库。

使用内存数据库测试

1、默认情况下,@DataJpaTest使用的是内存数据库进行测试,你无需配置和启用真实的数据库。只需要在 pom.xml 配置文件中声明如下依赖即可:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

gradle file:

testCompile('com.h2database:h2')

2、编写测试方法:

@RunWith(SpringRunner.class)
@DataJpaTest
public class EmployeeDaoTest {

    @Autowired
    private EmployeeDao employeeDao;

    @Test
    public void testSave() {
        Employee employee = new Employee();
        EmployeeDetail detail = new EmployeeDetail();
        detail.setName("kronchan");
        detail.setAge(24);
        employee.setDetail(detail);
        assertThat(detail.getName(), Matchers.is(employeeDao.save(employee).getDetail().getName()));;
    }
}

使用真实数据库测试

如要需要使用真实环境中的数据库进行测试,需要替换掉默认规则,使用@AutoConfigureTestDatabase(replace = Replace.NONE)注解:

@RunWith(SpringRunner.class)
@DataJpaTest
// 加入 AutoConfigureTestDatabase 注解
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class EmployeeDaoTest {

    @Autowired
    private EmployeeDao employeeDao;

    @Test
    public void testSave() {
        Employee employee = new Employee();
        EmployeeDetail detail = new EmployeeDetail();
        detail.setName("kronchan");
        detail.setAge(24);
        employee.setDetail(detail);
        assertThat(detail.getName(), Matchers.is(employeeDao.save(employee).getDetail().getName()));;
    }
}

事务控制

执行上面的新增数据的测试,发现测试通过,但是数据库却并没有新增数据。默认情况下,在每个 JPA 测试结束时,事务会发生回滚。这在一定程度上可以防止测试数据污染数据库。

如果你不希望事务发生回滚,你可以使用@Rollback(false)注解,该注解可以标注在类级别做全局的控制,也可以标注在某个特定不需要执行事务回滚的方法级别上。

也可以显式的使用注解 @Transactional 设置事务和事务的控制级别,放大事务的范围。

写博客是为了总结记录,而不应为了花里胡哨的标榜什么。比如写了一个算法,尽量联系下应用场景;看了一段源码,想一下对应用层调用有什么影响,做到学以致用,避免眼高手低。
原文地址:https://www.cnblogs.com/cappuccino-jay/p/14979613.html