spring boot 学习笔记 (21) 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",则测试用例执行成功;若不是,则会执行失败,再也无需关注控制台输出内容。

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.print;
@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")).andReturn().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 实例,该实例则可以获取到请求的返回内容。
  • 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正常运行结束---");
    }

    //又一个测试方法
    //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 既可让项目具备各种测试功能。在微服务架构下严格采用三层测试覆盖,才能有效保证项目质量。

发布了107 篇原创文章 · 获赞 178 · 访问量 87万+

猜你喜欢

转载自blog.csdn.net/bihansheng2010/article/details/88025479
今日推荐