Spring REST實踐之客戶端和測試


RestTemplate

可參考spring實戰來寫這部分。

RestTemplate免於編寫乏味的樣板代碼,RestTemplate定義了33個與REST資源交互的方法,涵蓋了HTTP動作的各種形式,其實這些方法只有11個獨立的方法,而每一個方法都由3個重載的變種。

delete():在特定的URL上對資源執行HTTP DELETE操作
exchange():在URL上執行特定的HTTP方法,返回包含對象的ResponseEntity,這個對象是從響應體中映射得到的
execute():在URL上執行特定的HTTP方法,返回一個從響應體映射得到的對象
getForEntity():發送一個HTTP GET請求,返回的ResponseEntity包含了響應體所映射成的對象
getForObject():GET資源,返回的請求體將映射為一個對象
headForHeaders():發送HTTP HEAD請求,返回包含特定資源URL的HTTP頭
optionsForAllow():發送HTTP OPTIONS請求,返回對特定URL的Allow頭信息
postForEntity():POST數據,返回包含一個對象的ResponseEntity,這個對象是從響應體中映射得到
postForLocation():POST數據,返回新資源的URL
postForObject():POST數據,返回的請求體將匹配為一個對象
put():PUT資源到特定的URL

除了TRACE,RestTemplate涵蓋了所有的HTTP動作。除此之外,execute()和exchange()提供了較低層次的通用方法來使用任意的HTTP方法。

每個方法都以3種方法進行了重載:

一個使用java.net.URI作為URL格式,不支持參數化URL
一個使用String作為URL格式,並使用Map指明URL參數
一個使用String作為URL格式,並使用可變參數列表指明URL參數

GET資源

有兩種執行GET請求的方法:getForObject()和getForEntity()。3個getObject()方法的簽名如下:

<T> T getForObject(URI url, Class<T> responseType) throws RestClientException;
<T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;

類似地,getForEntity()方法的簽名如下:

<T> ResponseEntity<T> getForObject(URI url, Class<T> responseType) throws RestClientException;
<T> ResponseEntity<T> getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> ResponseEntity<T> getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;

除了返回類型,getForObject()方法就是getForEntity()方法的鏡像。實際上,它們的工作方式大同小異。它們都執行根據URL檢索資源的GET請求。它們都將資源根據responseType參數匹配為一定的類型。唯一的區別在於getForObject()只返回所請求類型的對象,而getForEntity()方法會返回請求的對象以及響應的額外信息。

public Spittle[] retrieveSpittlesForSpitter(String username) {
	return new RestTemplate().getForObject("http://localhost:8080/Spitter/{spitter}/spittles", 
		Spittle[].class, username);
}

public Spittle[] retrieveSpittlesForSpitter(String username) {
	ResponseEntity<Spittle[]> reponse = new RestTemplate().getForEntity(
		"http://localhost:8080/Spitter/{spitter}/spittles",
		Spittle[].class, username);

	if(reponse.getStatusCode() == HttpStatus.NOT_MODIFIED) {
		throw new NotModifiedException();
	}

	return reponse.getBody();
}

PUT資源

void put(URI url, Object request) throws RestClientException;
void put(String url, Object request, Object... uriVairables) throws RestClientException;
void put(String url, Object request, Map<String, ?> uriVariables) throws RestClientException;

public void updateSpittle(Spittle spittle) throws SpitterException {
	try {
		String url = "http://localhost:8080/Spitter/spittles/" + spittle.getId();
		new RestTemplate().put(new URI(url), spittle);
	} catch(URISyntaxException e) {
		throw new SpitterUpdateException("Unable to update Spittle", e);
	}
}

public void updateSpittle(Spittle spittle) throws SpitterException {
	restTemplate.put("http://localhost:8080/Spitter/spittles/{id}",
		spittle, spittle.getId());
}

public void updateSpittle(Spittle spittle) throws SpitterException {
	Map<String, String> params = new HashMap<String, String>();
	params.put("id", spittle.getId());
	restTemplate.put("http://localhost:8080/Spitter/spittles/{id}",
		spittle, params);
}

DELETE資源

void delete(String url, Object... uriVariables) throws RestClientException;
void delete(String url, Map<String, ?> uriVariables) throws RestClientException;
void delete(URI url) throws RestClientException;

public void deleteSpittle(long id) {
	try {
		restTemplate.delete(new URI("http://localhost:8080/Spitter/spittles/" + id));
	} catch(URISyntaxException e) {

	}
}

POST資源數據

POST請求有postForObject()和postForEntity()兩種方法,和GET請求的getForObject()和getForEntity()方法類似。getForLocation()是POST請求所特有的。

<T> T postForObject(URI url, Object request, Class<T> responseType) throws RestClientException;
<T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> T postForObject(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;

上面三個方法中,第一個參數都是資源要POST到的URL,第二個參數是要發送的對象,而第三個參數是預期返回的Java類型。在URL作為String類型的兩個版本中,第四個參數指定了URL變量(要么是可變參數列表,要么是一個Map)。

<T> T postForObject(URI url, Object request, Class<T> responseType) throws RestClientException;
<T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> T postForObject(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;

ResponseEntity<Spitter> response = new RestTemplate().postForEntity("http://localhost:8080/Spitter/spitters",
	spitter, Spitter.class);
Spitter spitter = response.getBody();
URI url = response.getHeaders().getLocation();

postForLacation()會在POST請求的請求體中發送一個資源到服務器端,返回的不再是資源對象,而是創建資源的位置。

URI postForLocation(String url, Object request, Object... uriVariables) throws RestClientException;
URI postForLocation(String url, Object request, Map<String, ?> uriVariables) throws RestClientException;
URI postForLocation(URI url, Object request) throws RestClientException;

public String postSpitter(Spitter spitter) {
	RestTemplate rest = new RestTemplate();
	return rest.postForLocation("http://localhost:8080/Spitter/spitters",
		spitter).toString();
}

交換資源

exchange方法可以在發送個服務器端的請求中設置頭信息。

<T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType) throws RestClientException;

<T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throws RestClientException;

<T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;


MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
headers.add("Accept", "application/json");
HttpEntity<Object> requestEntity = new HttpEntity<Object>(headers);

ResponseEntity<Spitter> response = rest.exchange("http://localhost:8080/Spitter/spitters/{spitter}",
	HttpMethod.GET, requestEntity, Spitter.class, spitterId);

Testing REST Services

Spring框架提供了spring-test模塊,spring-test模塊為JNDI,Servlet和Portlet API提供了一系列的注解,工具類和mock對象。此框架同時也提供了跨測試執行過程的緩存應用上下文功能。為了能夠在非Spring Boot工程中使用spring-test模塊,你需要包含如下依賴:

<dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>4.1.6.RELEASE</version>
        <scope>test</scope>
</dependency>

Spring Boot提供了spring-boot-starter-test,它自動在Boot應用中增加了spring-test模塊,同時starter POM也包含了JUnit,Mockito和Hamcrest庫:

Mockito是一款流行的mocking框架。它提供了簡單的API用於創建和配置mock。
Hamcrest是一款為創建matcher提供了強大詞匯的框架。matcher允許你將一個對象和期望的執行的結果聯系起來。Matcher使得斷言更加刻度,同時它們也產生有意義的錯誤信息,當斷言失敗時。

為了更好地理解spring-test模塊,下面是測試用例:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = QuickPollApplication.class)
@WebAppConfiguration
public class ExampleTest {
}
@Before
public void setup() { }
@Test
public void testSomeThing() {}
@After
public void teardown() { }

@RunWith注解用於指定具體測試類,@ContextConfiguration用於為SpringJUnit4ClassRunner指定使用哪個XML配置文件。在上例中,@SpringApplicationConfiguration是提供了附加的Spring Boot特性的特殊的ContextConfiguration版本。@WebAppConfiguration指導Spring創建web應用上下文,即WebApplicationContext。

Unit Testing REST Controllers

Spring的依賴注入使得單元測試變得非常簡單。依賴能夠輕松用來模擬事先定義好的行為,因此允許我們孤立的測試代碼。

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.ArrayList;
import com.google.common.collect.Lists;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
public class PollControllerTestMock {
    @Mock
    private PollRepository pollRepository;

    @Before
    public void setUp() throws Exception {
                MockitoAnnotations.initMocks(this);
	}
    
    @Test
    public void testGetAllPolls() {
        PollController pollController  = new PollController();
		ReflectionTestUtils.setField(pollController, "pollRepository", pollRepository);
		when(pollRepository.findAll()).thenReturn(new ArrayList<Poll>());
		ResponseEntity<Iterable<Poll>> allPollsEntity = pollController.getAllPolls();
		verify(pollRepository, times(1)).findAll();
		assertEquals(HttpStatus.OK, allPollsEntity.getStatusCode());
		assertEquals(0, Lists.newArrayList(allPollsEntity.getBody()).size());
	}
}

Spring MVC Test framework Basics

Spring MVC測試框架包含四個重要的類:MockMvc,MockMvcRequestBuilders,MockMvcResultMatchers和MockMvcBuilders。org.springframework.test.web.servlet.MockMvc類是Spring MVC測試框架的核心,它能夠執行HTTP請求。它只包含了perform方法:

public ResultActions perform(RequestBuilder requestBuilder) throws java.lang.Exception

RequestBuilder提供了創建GET、POST等請求的抽象接口。為了簡化請求的構建,Spring MVC框架提供了org.springframework.test.web. servlet.request.MockHttpServletRequestBuilder實現,而且在此類中提供了helper靜態方法集合。

post("/test_uri")
 .param("admin", "false")
 .accept(MediaType.APPLICATION_JSON)
 .content("{JSON_DATA}");

上例中post方法用來創建POST請求。MockMvcRequestBuilder也提供了創建get、delete和put等請求的方法。param方法屬於MockHttpServletRequestBuilder類,用來為請求增加參數。MockHttpServletRequestBuilder類還提供了accept、content和header等用於向請求增加data和metadata的方法。

perform方法返回org.springframework.test.web.servlet.ResultActions對象,此對象可被用來在響應上執行斷言操作。

mockMvc.perform(post("/test_uri"))
   .andExpect(status().isOk())
   .andExpect(content().contentType(MediaType.APPLICATION_JSON))
   .andExpect(content().string("{JSON_DATA}"));

status方法驗證響應的狀態值。content方法用來楊崢響應體。

MockMvcBuilders類提供了兩種方式構建MockMvc對象:

webAppContextSetup:利用已初始化好的WebApplicationContext構建MockMvc。和上下文相關的配置信息會在MockMvc對象創建以前加載完成。這個技術被用於end-to-end測試。
standaloneSetup:不用加載任何spring配置構建MockMvc,為測試控制器只加載基本的MVC構件。此技術被用於單元測試。

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.mock.web.MockServletContext;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = QuickPollApplication.class)
@ContextConfiguration(classes = MockServletContext.class)
@WebAppConfiguration
public class PollControllerTest {
    @InjectMocks
    PollController pollController;
    @Mock
    private PollRepository pollRepository;
    private MockMvc mockMvc;
    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mockMvc = standaloneSetup(pollController).build();
	}
    @Test
    public void testGetAllPolls() throws Exception {
        when(pollRepository.findAll()).thenReturn(new ArrayList<Poll>());
		mockMvc.perform(get("/v1/polls"))
	        .andExpect(status().isOk())
	        .andExpect(content().string("[]"));
	} 
}



import static org.hamcrest.Matchers.hasSize;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;
import com.apress.QuickPollApplication;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = QuickPollApplication.class)
@WebAppConfiguration
public class PollControllerIT {
    @Inject
    private WebApplicationContext webApplicationContext;
    private MockMvc mockMvc;
    @Before
    public void setup() {
		mockMvc = webAppContextSetup(webApplicationContext).build();
	}

@Test
public void testGetAllPolls() throws Exception {
	mockMvc.perform(get("/v1/polls"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$", hasSize(20)));
    } 
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM