在微服务架构下,整个系统被切割为 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();
 }
}
同时在测试类中注⼊ HelloService sayHelloTest 测试⽅法中调⽤ 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 5 Spring 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 既可让项⽬具备各种测试功能。在微服务架构下
严格采⽤三层测试覆盖,才能有效保证项⽬质量。
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐