第 5-4 课: Spring Boot 对测试的⽀持
在微服务架构下,整个系统被切割为 N 个独⽴的微服务相互配合来使⽤,那么对于系统可⽤性会有更⾼的要求。从⼤到⼩可以分为三个层级,开发⼈员编码需要做的单元测试、微服务和微服务之间的接⼝联调测试、微服务和微服务之间的集成测试,通过三层的严格测试才能有效保证系统的稳定性。作为⼀名开发⼈员,严格做好代码的单元测试才是保证软件质量的第⼀步。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 的断⾔库;
- JsonPath,JSON 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
既可让项⽬具备各种测试功能。在微服务架构下
严格采⽤三层测试覆盖,才能有效保证项⽬质量。
更多推荐
已为社区贡献2条内容
所有评论(0)