欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

spring boot 学习笔记 (21) Spring Boot 对测试的支持

程序员文章站 2022-03-07 22:40:37
...

在微服务架构下,整个系统被切割为 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 既可让项目具备各种测试功能。在微服务架构下严格采用三层测试覆盖,才能有效保证项目质量。

相关标签: 测试