SpringBoot(五)SpringBoot的單元測試

本文已同步至我的博客 liaosi's blog-SpringBoot(五)SpringBoot的單元測試

在開發工做中,一般寫好代碼後咱們都會先自測一遍再交給測試部門,自測的方法有多種,也有多種測試工具,好比Postman、Jmeter等,這篇文章主要講對於SpringBoot項目如何使用SpringBoot的單元測試,使用的SpringBoot版本是1.5.7。java

一.示例項目介紹

建立一個SpringBoot的Maven項目,個人項目結構爲:
項目結構圖git

SpringBoot的單元測試須要額外添加的依賴是:github

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

        //其它依賴此處省略...

下面給出項目的代碼部分。
Javabean類:Book.javaweb

package com.lzumetal.springboot.demodatabase.entity;

public class Book {

    private Integer id;     //數據庫主鍵id標識
    private String name;    //書名
    private String author;  //做者
    private Double price;   //價格

    //get、set方法省略
}

dao類:BookMapper.javaspring

package com.lzumetal.springboot.demodatabase.mapper;


import com.lzumetal.springboot.demodatabase.entity.Book;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface BookMapper {

    int insert(Book record);
    List<Book> selectAll();
    Book getById(@Param(value = "id") Integer id);
}

對應的xml映射文件:BookMapper.xml數據庫

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lzumetal.springboot.demodatabase.mapper.BookMapper">
  <resultMap id="BaseResultMap" type="Book">
    <result column="id" jdbcType="INTEGER" property="id" />
    <result column="name" jdbcType="VARCHAR" property="name" />
    <result column="author" jdbcType="VARCHAR" property="author" />
    <result column="price" jdbcType="DOUBLE" property="price" />
  </resultMap>
  <insert id="insert" parameterType="Book">
    insert into book (id, name, author, 
      price)
    values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}, #{author,jdbcType=VARCHAR}, 
      #{price,jdbcType=DOUBLE})
  </insert>
  <select id="selectAll" resultMap="BaseResultMap">
    select id, name, author, price
    from book
  </select>
  <select id="getById" resultMap="BaseResultMap">
    select id, name, author, price
    from book
    WHERE id = #{id}
  </select>
</mapper>

Service類:BookServer.javaapache

package com.lzumetal.springboot.demodatabase.service;

import com.lzumetal.springboot.demodatabase.entity.Book;
import com.lzumetal.springboot.demodatabase.mapper.BookMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * Created by liaosi on 2017/9/26.
 */
@Service
public class BookService {

    @Autowired
    private BookMapper bookMapper;

    public List<Book> getAllBooks() {
        return bookMapper.selectAll();
    }

    public Book getById(Integer id) {
        return bookMapper.getById(id);
    }
}

Controller類:BookController.javajson

package com.lzumetal.springboot.demodatabase.controller;

import com.google.gson.Gson;
import com.lzumetal.springboot.demodatabase.entity.Book;
import com.lzumetal.springboot.demodatabase.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * Created by liaosi on 2017/9/26.
 */
@RestController
public class BookController {

    private static Gson gson = new Gson();

    @Autowired
    private BookService bookService;

    /**
     * GET請求+@PathVariable
     * @param id
     * @return
     */
    @RequestMapping(value = "/getBook/{id}", method = RequestMethod.GET)
    public String getBookInfo(@PathVariable("id") Integer id) {
        return gson.toJson(bookService.getById(id));
    }


    /**
     * GET請求
     * @param id
     * @return
     */
    @RequestMapping(value = "/getBookInfo2", method = RequestMethod.GET)
    public String getBoodInfo2(Integer id, String name) {
        Book book = new Book();
        book.setId(id);
        book.setName(name);
        return gson.toJson(book);
    }


    /**
     * 普通form表單POST請求
     * @param id
     * @return
     */
    @RequestMapping(value = "/postBookInfo", method = RequestMethod.POST)
    public String postBoodInfo(Integer id) {
        return gson.toJson(bookService.getById(id));
    }


    /**
     * POST請求,參數爲json格式
     * @param book
     * @return
     */
    @RequestMapping(value = "/postJson", method = RequestMethod.POST)
    public Book postJson(@RequestBody Book book) {
        return book;
    }

}

SpringBoot項目的啓動類:StartupApplication.javabootstrap

package com.lzumetal.springboot.demodatabase;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
// mapper 接口類包掃描
@MapperScan(basePackages = "com.lzumetal.springboot.demodatabase.mapper")
public class StartupApplication {

    public static void main(String[] args) {
        SpringApplication.run(StartupApplication.class, args);
    }
}

二.測試類

參考官網文檔:Testing improvements in Spring Boot 1.4springboot

1.測試Service或者Controller

MainTest.java

package com.lzumetal.springboot.demodatabase.test;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.lzumetal.springboot.demodatabase.StartupApplication;
import com.lzumetal.springboot.demodatabase.controller.BookController;
import com.lzumetal.springboot.demodatabase.entity.Book;
import com.lzumetal.springboot.demodatabase.service.BookService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

/*
https://spring.io/blog/2016/04/15/testing-improvements-in-spring-boot-1-4
MOCK —提供一個Mock的Servlet環境,內置的Servlet容器並無真實的啓動,主要搭配使用@AutoConfigureMockMvc

RANDOM_PORT — 提供一個真實的Servlet環境,也就是說會啓動內置容器,而後使用的是隨機端口
DEFINED_PORT — 這個配置也是提供一個真實的Servlet環境,使用的默認的端口,若是沒有配置就是8080
NONE — 這是個神奇的配置,跟Mock同樣也不提供真實的Servlet環境。
 */

@RunWith(SpringRunner.class)
@SpringBootTest(classes = StartupApplication.class)
public class MainTest {

    private static Gson gson = new GsonBuilder().setPrettyPrinting().create();

    @Autowired
    private BookService bookService;

    @Autowired
    private BookController bookController;


    @Test
    public void testBookService() {
        List<Book> allBooks = bookService.getAllBooks();
        System.out.println(gson.toJson(allBooks));
    }

    @Test
    public void testBookController() {
        String s = bookController.getBookInfo(1);
        System.out.println(s);
    }

}

官網上的說明:

@RunWith(SpringRunner.class) tells JUnit to run using Spring’s testing support. SpringRunner is the new name for SpringJUnit4ClassRunner, it’s just a bit easier on the eye.
@SpringBootTest is saying 「bootstrap with Spring Boot’s support」 (e.g. load application.properties and give me all the Spring Boot goodness)
The webEnvironment attribute allows specific 「web environments」 to be configured for the test. You can start tests with a MOCK servlet environment or with a real HTTP server running on either a RANDOM_PORT or a DEFINED_PORT.
If we want to load a specific configuration, we can use the classes attribute of @SpringBootTest. In this example, we’ve omitted classes which means that the test will first attempt to load @Configuration from any inner-classes, and if that fails, it will search for your primary @SpringBootApplication class.
  • @RunWith 是junit提供的註解,表示該類是單元測試的執行類
  • SpringRunner是spring-test提供的測試執行單元類(是Spring單元測試中SpringJUnit4ClassRunner的新名字)
  • SpringBootTest 是執行測試程序的引導類

2.模擬發送REST請求測試

RestTemplate

在java代碼裏進行REST請求測試,經常使用的好比Apache的HttpClient,可是spring也提供了一種簡單便捷的模板類RestTemplate來進行操做。

RestTemplate是Spring提供的一個web層測試模板類,經過RestTemplate能夠很方便地進行web層功能測試。它支持REST風格的URL,並且具備AnnotationMethodHandlerAdapter的數據轉換器HttpMessageConverters的裝配功能。RestTemplate已默認幫咱們完成了一下數據轉換器的註冊:

  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • SourceHttpMessageConverter
  • XmlAwareFormHttpMessageConverter

在默認狀況下,咱們能夠直接利用以上轉換器對響應數據進行轉換處理。如StringHttpMessageConverter來處理text/plain;MappingJackson2HttpMessageConverter來處理application/json;MappingJackson2XmlHttpMessageConverter來處理application/xml

而若是咱們像拓展其餘的轉換器如Jaxb2RootElementHttpMessageConverter或MappingJacksonHttpMessageConverter。咱們可使用setMessageConverters(List<HttpMessageConverter<?>> messageConverters)來註冊咱們所需的轉換器。

RestTemplate restTemplate = new RestTemplate();
        //獲取RestTemplate默認配置好的全部轉換器
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        //默認的MappingJackson2HttpMessageConverter在第7個 先把它移除掉
        messageConverters.remove(6);
        //添加上GSON的轉換器
        messageConverters.add(6, new GsonHttpMessageConverter());

這個簡單的例子展現瞭如何使用GsonHttpMessageConverter替換掉默認用來處理application/json的MappingJackson2HttpMessageConverter。

RestTemplate默認(即便用無參構造器建立實例)是使用java.net包中的標準Java類做爲底層實現來建立HTTP請求。可是能夠調用它的帶ClientHttpRequestFactory參數的構造器,使用 Apache 的 HttpComponents 或 Netty 和 OkHttp等其它HTTP請求庫。

配置默認實例:

@Bean
public RestTemplate restTemplate(){
    return new RestTemplate();
}

配置定製實例,構造方法中能夠傳入ClientHttpRequestFactory參數,ClientHttpRequestFactory接口的實現類中存在timeout屬性等

@Bean
RestTemplate restTemplate(){
        //生成一個設置了鏈接超時時間、請求超時時間、異常最大重試次數的httpClient
        RequestConfig config = RequestConfig.custom()
                            .setConnectionRequestTimeout(10000)
                            .setConnectTimeout(10000)
                            .setSocketTimeout(30000)
                            .build();
        HttpClient httpClient = HttpClientBuilder.create()
                            .setDefaultRequestConfig(config)
                            .setRetryHandler(new DefaultHttpRequestRetryHandler(5, false))
                            .build();
        
        //使用httpClient建立一個ClientHttpRequestFactory的實現
        ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);

         //ClientHttpRequestFactory做爲參數構造一個使用做爲底層的RestTemplate
        RestTemplate restTemplate = new RestTemplate(requestFactory);

TestRestTemplate

TestRestTemplate是SpringBoot提供的一個測試模板類,在SpringBoot你既可使用RestTemplate,同時也可使用TestRestTemplate,TestRestTemplate是RestTemplate的一個包裝類,而沒有繼承它,因此不會存在bean注入的問題。若是想在TestRestTemplate中獲取,能夠調用它的getRestTemplate()方法。在使用了SpringBootTest註解的狀況下,TestRestTemplate能夠直接使用@Autowired注入。

在下面的示例中主要介紹如何使用TestRestTemplate進行post和get請求測試。若是想使用RestTemplate也差很少是同樣的方式。

UrlRequestTest.java

package com.lzumetal.springboot.demodatabase.test;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.lzumetal.springboot.demodatabase.StartupApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.util.HashMap;
import java.util.Map;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = StartupApplication.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class UrlRequestTest {


    private static Gson gson = new GsonBuilder().setPrettyPrinting().create();

    @Autowired
    private TestRestTemplate testRestTemplate;

    /**
     * GET請求+@PathVariable
     */
    @Test
    public void getRequest() {
        ResponseEntity<String> entity = testRestTemplate.getForEntity("/getBook/{id}", String.class, 1);
        System.err.println(entity.getBody());
    }


    /**
     * GET請求
     * getForEntity 和 getForObject 的區別:
     * getForObject返回結果Controller中的返回類型。
     * getForEntity返回結果裏包含了請求頭信息等,同時entity.getBody()的結果已經被轉換成了json字符串
     */
    @Test
    public void getRequest2() {
        String result = testRestTemplate.getForObject("/getBookInfo2?id={id}&name={2}", String.class, 100, "解憂雜貨店");
        System.err.println(result);
    }


    /**
     * GET請求除了使用佔位符的方式按次序注入,也能夠經過一個map經過名字注入
     */
    @Test
    public void getRequest3() {
        Map<String, Object> param = new HashMap<>();
        param.put("bookid", 20);
        param.put("name", "呼嘯山莊");
        String result = testRestTemplate.getForObject("/getBookInfo2?id={bookid}&name={name}", String.class, param);
        System.err.println(result);
    }


    /**
     * POST請求
     */
    @Test
    public void postRequest() {
        MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
        param.add("id",2);
        String result = testRestTemplate.postForObject("/postBookInfo", param, String.class);
        System.err.println(result);
    }

    /**
     * POST請求,並帶請求頭
     */
    @Test
    public void postRequest2() {
        HttpHeaders headers = new HttpHeaders();
        headers.add("token", "aaaaaaabbbbbbdcccc");

        MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
        param.add("id",2);

        HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(param, headers);

        ResponseEntity<String> resultEntity = testRestTemplate.postForEntity("/postBookInfo", entity, String.class);
        System.err.println("reuslt:" + resultEntity.getBody());
        System.err.println("headers:" + resultEntity.getHeaders());
    }


    /**
     * POST請求,入參是json格式字符串:{"id":2,"name":"Effective Java","author":"Joshua Bloch","price":39.0}
     */
    @Test
    public void postRequest3() {
        String jsonStr = "{\"id\":2,\"name\":\"Effective Java\",\"author\":\"Joshua Bloch\",\"price\":39.0}";
        HttpHeaders headers = new HttpHeaders();

        //設置contentType
        headers.setContentType(MediaType.valueOf("application/json;UTF-8"));

        HttpEntity<String> entity = new HttpEntity<String>(jsonStr,headers);
        String result = testRestTemplate.postForObject("/postJson", entity, String.class);
        System.err.println(result);
    }

        /**
     * 上傳文件
     *
     * @throws Exception
     */
    @Test
    public void upload() throws Exception {
        Resource resource = new FileSystemResource("d:/123.jpg");
        MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
        param.add("files", resource);
        String result = testRestTemplate.postForObject("/uploadFile", param, String.class);
        System.out.println(result);
    }


    /**
     * 下載文件
     *
     * @throws Exception
     */
    @Test
    public void download() throws Exception {
        HttpHeaders headers = new HttpHeaders();
        headers.set("token", "xxxxxx");
        HttpEntity formEntity = new HttpEntity(headers);

        ResponseEntity<byte[]> response = testRestTemplate.exchange("/download?file={1}", HttpMethod.GET, formEntity, byte[].class, "d:/aaa.png");
        if (response.getStatusCode() == HttpStatus.OK) {
            FileUtils.writeByteArrayToFile(new File("d:/123.jpg"), response.getBody());
        }
    }


}

官網上的說明:

Note that TestRestTemplate is now available as bean whenever @SpringBootTest is used. It’s pre-configured to resolve relative paths to http://localhost:${local.server.port}. We could have also used the @LocalServerPort annotation to inject the actual port that the server is running on into a test field.

關於webEnvironment,有MOCK、RANDOM_PORT、DEFINED_PORT、NONE四個選項,其中:

  • MOCK —提供一個虛擬的Servlet環境,內置的Servlet容器並無真實的啓動
  • RANDOM_PORT — 提供一個真實的Servlet環境,也就是說會啓動內置容器,而後使用的是隨機端口
  • DEFINED_PORT — 這個配置也是提供一個真實的Servlet環境,使用的配置文件中配置的端口,若是沒有配置,默認是8080

關於SpringBoot的單元測試,可能也還會用到@Before等其它註解,本文再也不全面而深刻的研究,僅展現簡單示例供使用參考。


本文示例代碼已上傳到GitHub: https://github.com/liaosilzu2...

相關文章
相關標籤/搜索