第 5-4 課: Spring Boot 對測試的⽀持

在微服務架構下,整個系統被切割爲 N 個獨⽴的微服務相互配合來使⽤,那麼對於系統可⽤性會有更⾼的要
求。從⼤到⼩可以分爲三個層級,開發⼈員編碼需要做的單元測試、微服務和微服務之間的接⼝聯調測試、
微服務和微服務之間的集成測試,通過三層的嚴格測試纔能有效保證系統的穩定性。
 
作爲⼀名開發⼈員,嚴格做好代碼的單元測試纔是保證軟件質量的第⼀步。Spring Boot 做爲⼀個優秀的開源
框架合集對測試的⽀持⾮常友好,Spring Boot 提供了專⻔⽀持測試的組件 Spring Boot Test,其集成了業內
流⾏的 7 種強⼤的測試框架:
  • JUnit,⼀個 Java 語⾔的單元測試框架;
  • Spring Test,爲 Spring Boot 應⽤提供集成測試和⼯具⽀持;
  • AssertJ,⽀持流式斷⾔的 Java 測試框架;
  • Hamcrest,⼀個匹配器庫;
  • Mockito,⼀個 Java Mock 框架;
  • JSONassert,⼀個針對 JSON 的斷⾔庫;
  • JsonPathJSON XPath 庫。
7 種測試框架完整的⽀持了軟件開發中各種場景,我們只需要在項⽬中集成 Spring Boot Test 即可擁有這
7 種測試框架的各種功能,並且 Spring 針對 Spring Boot 項⽬使⽤場景進⾏了封裝和優化,以⽅便在 Spring
Boot 項⽬中去使⽤,接下來介紹 Spring Boot Test 的使⽤。

快速⼊⼿

我們創建⼀個 spring-boot-test 項⽬來演示 Spring Boot Test 的使⽤,只需要在項⽬中添加 spring-boot
starter-test 依賴即可:
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-test</artifactId>
</dependency>
測試⽅法
⾸先來演示⼀個最簡單的測試,只是測試⼀個⽅法的執⾏:
 
public class HelloTest {
 @Test
 public void hello() {
 System.out.println("hello world");
 }
}
Idea 中點擊 helle() ⽅法名,選擇 Run hello() 即可運⾏,執⾏完畢控制檯打印信息如下:
hello world
證明⽅法執⾏成功。

測試服務

⼤多數情況下都是需要測試項⽬中某⼀個服務的準確性,這個時候往往需要 Spring Boot 啓動後的上下⽂環
境,對於這種情況只需要添加兩個註解即可⽀持。我們創建⼀個 HelloService 服務來演示。
public interface HelloService {
 public void sayHello();
}
創建⼀個它的實現類:
 
@Service
public class HelloServieImpl implements HelloService {
 @Override
 public void sayHello() {
 System.out.println("hello service");
 }
}
在這個實現類中 sayHello() ⽅法輸出了字符串:"hello service"
爲了可以在測試中獲取到啓動後的上下⽂環境(Beans),Spring Boot Test 提供了兩個註解來⽀持,測試時
只需在測試類的上⾯添加 @RunWith(SpringRunner.class) @SpringBootTest 註解即可。
 
@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloServiceTest {
 @Resource
 HelloService helloService;
 @Test
 public void sayHelloTest(){
 helloService.sayHello();
 }
}
同時在測試類中注⼊ HelloServicesayHelloTest 測試⽅法中調⽤ HelloService sayHello() ⽅法,執⾏測
試⽅法後,就會發現在控制檯打印出了 Spring Boot 的啓動信息,說明在執⾏測試⽅法之前,Spring Boot
容器進⾏了初始化,輸出完啓動信息後會打印出以下信息:
hello service
證明測試服務成功,但是這種測試會稍顯麻煩,因爲控制檯打印了太多的東⻄,需要我們來仔細分辨,這⾥
有更優雅的解決⽅案,可以利⽤ OutputCapture 來判斷 System 是否輸出了我們想要的內容,添加
OutputCapture 改造如下。
import static org.assertj.core.api.Assertions.assertThat;
import org.springframework.boot.test.rule.OutputCapture;
@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloServiceTest {
 @Rule
 public OutputCapture outputCapture = new OutputCapture();
 @Resource
 HelloService helloService;
 @Test
 public void sayHelloTest(){
 helloService.sayHello();
 assertThat(this.outputCapture.toString().contains("hello service")).isTrue
();
 }
}
OutputCapture Spring Boot 提供的⼀個測試類,它能捕獲 System.out System.err 的輸出,我們可以利
⽤這個特性來判斷程序中的輸出是否執⾏。
 
這樣當輸出內容若是 "hello service",則測試⽤例執⾏成功;若不是,則會執⾏失敗,再也⽆需關注控制檯輸
出內容。GitChat

Web 測試

據統計現在開發的 Java 項⽬中 90% 以上都是 Web 項⽬,如何檢驗 Web 項⽬對外提供接⼝的準確性就變得
很重要。在以往的經歷中,我們常常會在瀏覽器中訪問⼀些特定的地址來進⾏測試,但如果涉及到⼀些⾮
get 請求就會變的稍微麻煩⼀些,有的讀者會使⽤ PostMan ⼯具或者⾃⼰寫⼀些 HTTP Post 請求來進⾏測
試,但終究不夠優雅⽅便。
 
Spring Boot Test 中有針對 Web 測試的解決⽅案:MockMvc,其實現了對 HTTP 請求的模擬,能夠直接使⽤
⽹絡的形式,轉換到 Controller 的調⽤,這樣可以使得測試速度更快、不依賴⽹絡環境,⽽且提供了⼀套驗
證的⼯具,這樣可以使得請求的驗證統⼀⽽且更⽅便。
 
接下來進⾏演示,⾸先在項⽬中添加 Web 依賴:
 
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-web</artifactId>
</dependency>
創建⼀個 HelloController 對外輸出⼀個 hello 的⽅法。
@RestController
public class HelloController {
 @RequestMapping(name="/hello")
 public String getHello() {
 return "hello web";
 }
}
創建 HelloWebTest 對我們上⾯創建的 web 接⼝ getHello() ⽅法進⾏測試。
 
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.pr
int;
@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloWebTest {
 private MockMvc mockMvc;
 @Before
 public void setUp() throws Exception {
 mockMvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
 }
 @Test
 public void testHello() throws Exception {
 mockMvc.perform(MockMvcRequestBuilders.post("/hello")
 .accept(MediaType.APPLICATION_JSON_UTF8)).andDo(print());
 }
}
  • @Before 注意意味着在測試⽤例執⾏前需要執⾏的操作,這⾥是初始化需要建⽴的測試環境。
  • MockMvcRequestBuilders.post 是指⽀持 post 請求,這⾥其實可以⽀持各種類型的請求,如 get 請求、 put 請求、patch 請求、delete 請求等。
  • andDo(print())andDo():添加 ResultHandler 結果處理器,print() 打印出請求和相應的內容。
控制檯輸出:
MockHttpServletRequest:
 HTTP Method = POST
 Request URI = /hello
 Parameters = {}
 Headers = {Accept=[application/json;charset=UTF-8]}
 Body = <no character encoding set>
 Session Attrs = {}
Handler:
 Type = com.neo.web.HelloController
 Method = public java.lang.String com.neo.web.HelloController.getUser()
Async:
 Async started = false
 Async result = null
Resolved Exception:
 Type = null
ModelAndView:
 View name = null
 View = null
 Model = null
FlashMap:
 Attributes = null
MockHttpServletResponse:
 Status = 200
 Error message = null
 Headers = {Content-Type=[application/json;charset=UTF-8], Content-Length
=[9]}
 Content type = application/json;charset=UTF-8
 Body = hello web
 Forwarded URL = null
 Redirected URL = null
 Cookies = []
通過上⾯輸出的信息會發現,將整個請求的過程全部打印了出來,包括請求頭信息、請求參數、返回信息
等,根據打印的 Body 信息可以得知 HelloController getHello() ⽅法測試成功。
 
但有時候我們並不想知道整個請求流程,只需要驗證返回的結果是否正確即可,可以做下⾯的改造:
 
@Test
public void testHello() throws Exception {
 mockMvc.perform(MockMvcRequestBuilders.post("/hello")
 .accept(MediaType.APPLICATION_JSON_UTF8))
// .andDo(print())
 .andExpect(content().string(equalTo("hello web")));
}
如果接⼝返回值是 "hello web" 測試執⾏成功,否則測試⽤例執⾏失敗。也⽀持驗證結果集中是否包含了特定
的字符串,這時可以使⽤ containsString() ⽅法來判斷。
.andExpect(content().string(containsString("hello")));;
⽀持直接將結果集轉換爲字符串輸出:
 
String mvcResult= mockMvc.perform(MockMvcRequestBuilders.get("/messages")).andRetu
rn().getResponse().getContentAsString();
System.out.println("Result === "+mvcResult);
⽀持在請求的時候傳遞參數:
 
@Test
public void testHelloMore() throws Exception {
 final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
 params.add("id", "6");
 params.add("hello", "world");
 mockMvc.perform(
 MockMvcRequestBuilders.post("/hello")
 .params(params)
 .contentType(MediaType.APPLICATION_JSON_UTF8)
 .accept(MediaType.APPLICATION_JSON_UTF8))
 .andExpect(status().isOk())
 .andExpect(content().string(containsString("hello")));;
}
返回結果如果是 JSON 可以使⽤下⾯語法來判斷:
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("純潔的微笑"))
MockMvc 提供⼀組⼯具函數⽤來執⾏ Assert 判斷,這組⼯具使⽤函數的鏈式調⽤,允許將多個測試⽤例拼
接在⼀起,同時進⾏多個判斷。
  • perform 構建⼀個請求,並且返回 ResultActions 實例,該實例則可以獲取到請求的返回內容。GitChat
  • params 構建請求時候的參數,也⽀持 param(key,value) 的⽅式連續添加。
  • contentType(MediaType.APPLICATION_JSON_UTF8) 代表發送端發送的數據格式。
  • accept(MediaType.APPLICATION_JSON_UTF8) 代表客戶端希望接受的數據類型格式。
  • mockMvc.perform() 建⽴ Web 請求。
  • andExpect(...) 可以在 perform(...) 函數調⽤後多次調⽤,表示對多個條件的判斷。
  • status().isOk() 判斷請求狀態是否返回 200
  • andReturn 該⽅法返回 MvcResult 對象,該對象可以獲取到返回的視圖名稱、返回的 Response 狀態、 獲取攔截請求的攔截器集合等。

JUnit 使⽤

JUnit 是針對 Java 語⾔的⼀個單元測試框架,它被認爲是迄今爲⽌所開發的最重要的第三⽅ Java 庫。 JUnit
的優點是整個測試過程⽆需⼈的參與、⽆需分析和判斷最終測試結果是否正確,⽽且可以很容易地⼀次性運
⾏多個測試。 JUnit 的最新版本爲 Junit 5Spring Boot 默認集成的是 Junit 4
 
以下爲 Junit 常⽤註解:
 
  • @Test,把⼀個⽅法標記爲測試⽅法
  • @Before,每⼀個測試⽅法執⾏前⾃動調⽤⼀次
  • @After,每⼀個測試⽅法執⾏完⾃動調⽤⼀次
  • @BeforeClass,所有測試⽅法執⾏前執⾏⼀次,在測試類還沒有實例化就已經被加載,因此⽤ static
  • @AfterClass,所有測試⽅法執⾏前執⾏⼀次,在測試類還沒有實例化就已經被加載,因此⽤ static
  • @Ignore,暫不執⾏該測試⽅法
  • @RunWith 當⼀個類⽤ @RunWith 註釋或繼承⼀個⽤ @RunWith 註釋的類時,JUnit 將調⽤它所引⽤的
類來運⾏該類中的測試⽽不是開發者再去 JUnit 內部去構建它。我們在開發過程中使⽤這個特性看看。
創建測試類 JUnit4Test類:
public class JUnit4Test {
 Calculation calculation = new Calculation();
 int result; //測試結果
 //在 JUnit 4 中使⽤ @Test 標註爲測試⽅法
 @Test
 //測試⽅法必須是 public void 的
 public void testAdd() {
 System.out.println("---testAdd開始測試---");
 //每個⾥⾯只測⼀次,因爲 assertEquals ⼀旦測試發現錯誤就拋出異常,不再運⾏後續代碼
 result = calculation.add(1, 2);
 assertEquals(3, result);
 System.out.println("---testAdd正常運⾏結束---");
GitChat
 }
 //⼜⼀個測試⽅法
 //timeout 表示測試允許的執⾏時間毫秒數,expected 表示忽略哪些拋出的異常(不會因爲該異常導
致測試不通過)
 @Test(timeout = 1, expected = NullPointerException.class)
 public void testSub() {
 System.out.println("---testSub開始測試---");
 result = calculation.sub(3, 2);
 assertEquals(1, result);
 throw new NullPointerException();
 //System.out.println("---testSub正常運⾏結束---");
 }
 //指示該[靜態⽅法]將在該類的[所有]測試⽅法執⾏之[前]執⾏
 @BeforeClass
 public static void beforeAll() {
 System.out.println("||==BeforeClass==||");
 System.out.println("||==通常在這個⽅法中加載資源==||");
 }
 //指示該[靜態⽅法]將在該類的[所有]測試⽅法執⾏之[後]執⾏
 @AfterClass
 public static void afterAll() {
 System.out.println("||==AfterClass==||");
 System.out.println("||==通常在這個⽅法中釋放資源==||");
 }
 //該[成員⽅法]在[每個]測試⽅法執⾏之[前]執⾏
 @Before
 public void beforeEvery() {
 System.out.println("|==Before==|");
 }
 //該[成員⽅法]在[每個]測試⽅法執⾏之[後]執⾏
 @After
 public void afterEvery() {
 System.out.println("|==After==|");
 }
}
calculation 是⾃定義的計算器⼯具類,具體可以參考示例項⽬,執⾏測試類後,輸出:
||==BeforeClass==||
||==通常在這個⽅法中加載資源==||
|==Before==|
---testAdd開始測試---
---testAdd正常運⾏結束---
|==After==|
|==Before==|
---testSub開始測試---
|==After==|
||==AfterClass==||
||==通常在這個⽅法中釋放資源==||
對⽐上⾯的介紹可以清晰的瞭解每個註解的使⽤。

Assert 使⽤

Assert 翻譯爲中⽂爲斷⾔,使⽤過 JUnit 的讀者都熟知這個概念,它斷定某⼀個實際的運⾏值和預期想⼀
樣,否則就拋出異常 。Spring 對⽅法⼊參的檢測借⽤了這個概念,其提供的 Assert 類擁有衆多按規則對⽅
法⼊參進⾏斷⾔的⽅法,可以滿⾜⼤部分⽅法⼊參檢測的要求。
 
Spring Boot 也提供了斷⾔式的驗證,幫助我們在測試時驗證⽅法的返回結果。
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestAssert {
 @Autowired
 private UserService userService;
 @Test
 public void TestAssert(){
 //驗證結果是否爲空
 Assert.assertNotNull(userService.getUser());
 //驗證結果是否相等
 Assert.assertEquals("i am neo!", userService.getUser());
 //驗證條件是否成⽴
 Assert.assertFalse(1+1>3);
 //驗證對象是否相等
 Assert.assertSame(userService,userService);
 int status=404;
 //驗證結果集,提示
 Assert.assertFalse("錯誤,正確的返回值爲200", status != 200);
 String[] expectedOutput = {"apple", "mango", "grape"};
 String[] methodOutput = {"apple", "mango", "grape1"};
 //驗證數組是否相同
 Assert.assertArrayEquals(expectedOutput, methodOutput);
 }
}
通過上⾯使⽤的例⼦可以發現,使⽤ Assert 可以⾮常⽅便驗證測試返回結果,避免寫很多的 if/else 判斷,讓 代碼更加的優雅。

如何使⽤ assertThat

JUnit 4 學習 JMock,引⼊了 Hamcrest 匹配機制,使得程序員在編寫單元測試的 assert 語句時,可以具有
更強的可讀性,⽽且也更加靈活。Hamcrest 是⼀個測試的框架,它提供了⼀套通⽤的匹配符 Matcher,靈活
使⽤這些匹配符定義的規則,程序員可以更加精確的表達⾃⼰的測試思想,指定所想設定的測試條件。
 
斷⾔便是 Junit 中最⻓使⽤的語法之⼀,在⽂章內容開始使⽤了 assertThat System 輸出的⽂本進⾏了判
斷,assertThat 其實是 JUnit 4 最新的語法糖,只使⽤ assertThat ⼀個斷⾔語句,結合 Hamcrest 提供的匹
配符,就可以替代之前所有斷⾔的使⽤⽅式。
 
assertThat 的基本語法如下:
 
assertThat( [value], [matcher statement] );
  • value 是接下來想要測試的變量值;
  • matcher statement 是使⽤ Hamcrest 匹配符來表達對前⾯變量所期望值的聲明,如果 value 值與 matcher statement 所表達的期望值相符,則測試成功,否則測試失敗。
⼀般匹配符
// allOf 匹配符表明如果接下來的所有條件必須都成⽴測試才通過,相當於“與”(&&)
assertThat( testedNumber, allOf( greaterThan(8), lessThan(16) ) );
// anyOf 匹配符表明如果接下來的所有條件只要有⼀個成⽴則測試通過,相當於“或”(||)
assertThat( testedNumber, anyOf( greaterThan(16), lessThan(8) ) );
// anything 匹配符表明⽆論什麼條件,永遠爲 true
assertThat( testedNumber, anything() );
// is 匹配符表明如果前⾯待測的 object 等於後⾯給出的 object,則測試通過
assertThat( testedString, is( "developerWorks" ) );
// not 匹配符和 is 匹配符正好相反,表明如果前⾯待測的 object 不等於後⾯給出的 object,則測試通
過
assertThat( testedString, not( "developerWorks" ) );
字符串相關匹配符
 
// containsString 匹配符表明如果測試的字符串 testedString 包含⼦字符串"developerWorks"則
測試通過
assertThat( testedString, containsString( "developerWorks" ) );
// endsWith 匹配符表明如果測試的字符串 testedString 以⼦字符串"developerWorks"結尾則測試通
過
assertThat( testedString, endsWith( "developerWorks" ) ); 
// startsWith 匹配符表明如果測試的字符串 testedString 以⼦字符串"developerWorks"開始則測試
通過
assertThat( testedString, startsWith( "developerWorks" ) ); 
// equalTo 匹配符表明如果測試的 testedValue 等於 expectedValue 則測試通過,equalTo 可以測
試數值之間的字
//符串之間和對象之間是否相等,相當於 Object 的 equals ⽅法
assertThat( testedValue, equalTo( expectedValue ) ); 
// equalToIgnoringCase 匹配符表明如果測試的字符串 testedString 在忽略⼤⼩寫的情況下等於
//"developerWorks"則測試通過
assertThat( testedString, equalToIgnoringCase( "developerWorks" ) ); 
// equalToIgnoringWhiteSpace 匹配符表明如果測試的字符串 testedString 在忽略頭尾的任意個空
格的情況下等
//於"developerWorks"則測試通過,注意,字符串中的空格不能被忽略
assertThat( testedString, equalToIgnoringWhiteSpace( "developerWorks" ) );
數值相關匹配符
// closeTo 匹配符表明如果所測試的浮點型數 testedDouble 在 20.0±0.5 範圍之內則測試通過
assertThat( testedDouble, closeTo( 20.0, 0.5 ) );
// greaterThan 匹配符表明如果所測試的數值 testedNumber ⼤於 16.0 則測試通過
assertThat( testedNumber, greaterThan(16.0) );
// lessThan 匹配符表明如果所測試的數值 testedNumber ⼩於 16.0 則測試通過
assertThat( testedNumber, lessThan (16.0) );
// greaterThanOrEqualTo 匹配符表明如果所測試的數值 testedNumber ⼤於等於 16.0 則測試通過
assertThat( testedNumber, greaterThanOrEqualTo (16.0) );
// lessThanOrEqualTo 匹配符表明如果所測試的數值 testedNumber ⼩於等於 16.0 則測試通過
assertThat( testedNumber, lessThanOrEqualTo (16.0) );
collection 相關匹配符
 
// hasEntry 匹配符表明如果測試的 Map 對象 mapObject 含有⼀個鍵值爲"key"對應元素值爲"value"
的 Entry 項則
//測試通過
assertThat( mapObject, hasEntry( "key", "value" ) );
// hasItem 匹配符表明如果測試的迭代對象 iterableObject 含有元素“element”項則測試通過
assertThat( iterableObject, hasItem ( "element" ) );
// hasKey 匹配符表明如果測試的 Map 對象 mapObject 含有鍵值“key”則測試通過
assertThat( mapObject, hasKey ( "key" ) );
// hasValue 匹配符表明如果測試的 Map 對象 mapObject 含有元素值“value”則測試通過
assertThat( mapObject, hasValue ( "key" ) );
具體使⽤可參考示例項⽬中 CalculationTest 的使⽤。
Junt 使⽤的⼏條建議:
  • 測試⽅法上必須使⽤ @Test 進⾏修飾
  • 測試⽅法必須使⽤ public void 進⾏修飾,不能帶任何的參數
  • 新建⼀個源代碼⽬錄來存放我們的測試代碼,即將測試代碼和項⽬業務代碼分開
  • 測試類所在的包名應該和被測試類所在的包名保持⼀致
  • 測試單元中的每個⽅法必須可以獨⽴測試,測試⽅法間不能有任何的依賴
  • 測試類使⽤ Test 作爲類名的後綴(不是必須)
  • 測試⽅法使⽤ Test 作爲⽅法名的前綴(不是必須)

總結

Spring Boot 是⼀款⾃帶測試組件的開源軟件,Spring Boot Test 中內置了 7 種強⼤的測試⼯具,覆蓋了測試
中的⽅⽅⾯⾯,在實際應⽤中只需要導⼊ Spring Boot Test 既可讓項⽬具備各種測試功能。在微服務架構下
嚴格採⽤三層測試覆蓋,纔能有效保證項⽬質量。
發佈了89 篇原創文章 · 獲贊 19 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章