zoukankan      html  css  js  c++  java
  • SpringBoot系列: 单元测试2

    之前发了SpringBoot 单元测试的博客, https://www.cnblogs.com/harrychinese/p/springboot_unittesting.html , 内容较少, 现在补齐SpringBoot单元测试的主要知识点. 

    测试有很多种, 有单元测试 、集成测试 、冒烟测试 、回归测试 、端到端测试 、功能测试。 它们作用不同, 但又有所重叠.

    1. 单元测试: [责任人: 开发人员]针对 class 级别的测试, 针对一个类, 写一个测试类. 在项目中, class 之间依赖调用是很常见的事情, 如果要测试的 classA, 又调用了classB, 这时候就没有遵守 "在一个class范围内测试", 自然不算单元测试, 应归为集成测试了. 不过这时候, 我们可以使用 mock 技术模拟被依赖的 classB, 这样整个测试又聚焦在classA 自身的功能, 所以又变回为单元测试.
    2. 集成测试: [责任人: 开发人员]是单元测试在粒度上更进一步, 测试不同class之间的组合功能.
    3. 冒烟测试: 可以理解为预测试, 一旦预测试失败(发现重大bug), 就直接停止测试, 打回给开发人员.
    4. 回归测试: 重跑原有测试用例.

    =====================
    spring-boot-starter-test 依赖包括:
    =====================
    1. JUnit, 测试框架
    2. Spring Test & Spring Boot Test,
    3. Mockito, Mock框架, 可以模拟任何 Spring 管理的 bean. 在单元测试过程中, 使用 @MockBean 注解可以将一个业务Service 的模拟器注入到我们要测试的SpringBoot程序中, 而不是真正的业务Service实现类.
    4. JSONassert,可以对json内容进行断言.
    5. JsonPath, 提供类类似于 XPath 的 json 路径访问形式.


    =====================
    JUnit 方法级别的注解
    =====================
    JUnit 4.x 全面引入了注解方式进行单元测试, 测试相关的方法都必须是 public .
    @BeforeClass -- 用来修饰static方法, 在测试类加载时执行.
    @AfterClass -- 用来修饰static方法, 在测试类结束时执行.
    @Before -- 用来修饰实例方法, 在每个测试方法之前执行, 比如用来初始化一个 MockMvc 对象.
    @After -- 用来修饰实例方法, 在每个测试方法之后执行.
    @Test -- 用来修饰实例方法, 是测试用例方法.
    @Transactional --和@Test一起搭配使用, 作用是自动回滚事务, 防止单元测试污染测试数据库.
    @Rollback(false) --和@Transactional/@Test一起搭配使用, 用来提交事务.

    =====================
    JUnit 类级别注解
    =====================
    1. @RunWith -- 对于SpringBoot程序, 使用 @RunWith(SpringRunner.class)
    2. @SuiteClasses -- 在IDE中一次只能执行一个测试类, 对于一个大型项目, 肯定不止一个测试类, 如何一次执行多个测试类呢? 答案就是使用@SuiteClasses. 该注解能将多个测试class归并到一个新的测试class, 新的测试类往往是一个空类, 需要加上 @RunWith(Suite.class), 代码如下:

    @RunWith(Suite.class) 
    @SuiteClasses({Test1.class, Test2.class})
    public class TestSuiteMain{
        // 空类, 但它会执行 TestSuite1 和 TestSuite2 的所有测试方法
    }


    =====================
    JUnit 断言
    =====================
    assertEquals() --是否相等
    assertSame() --判断是否是同一个对象
    assertTrue()
    assertFalse()
    assertNotNull()
    assertArrayEquals()
    assertThat() -- JUnit4.4 新增一个全能的断言

    =====================
    Spring test 提供的类级别注解
    =====================
    Spring 提供了一系列的测试方式注解, 我们可以按需选择它们, @SpringBootTest 主要用于集成测试, 其他注解用于单元测试.
    (1). @SpringBootTest, 该注解负责扫描配置来构建测试用的Spring上下文环境. 将在启动单元测试之前, 首先按照包名逐级向上找 SpringBoot 业务系统的入口, 并启动该SpringBoot 程序. 所以启动速度较慢, 用于集成测试.
    如果我们需要要注入 WebApplicationContext 和 MockMvc bean, 需要在类上再加一个 @AutoConfigureMockMvc 注解. 完整示例见下面的 MyControllerTests 代码. 

    (2). @WebMvcTest, 该注解仅仅扫描并初始化与 Controller 相关的bean, 但一般的@Component并不会被扫描, 另外也不启动一个 http server, 所以能加快测试进程. @WebMvcTest 还可以传入 Controller 类清单参数, 进一步缩小容器中bean的数量.
    @WebMvcTest 还会自动实例化一个 WebApplicationContext 和 MockMvc bean, 供我们在测试用例中使用. 

    
    

    (3). @RestClientTest 和 @WebMvcTest 类似, 用来测试基于Rest API的Service 类, 该注解会自动实例化一个MockRestServiceServer bean用来模拟远程的 Restful 服务.

    @RunWith(SpringRunner.class)
    @RestClientTest(RemoteVehicleDetailsService.class)
    public class ExampleRestClientTest {
    
        @Autowired
        private RemoteVehicleDetailsService service;
    
        @Autowired
        private MockRestServiceServer server;
    
        @Test
        public void getVehicleDetailsWhenResultIsSuccessShouldReturnDetails()
                throws Exception {
            this.server.expect(requestTo("/greet/details"))
                    .andRespond(withSuccess("hello", MediaType.TEXT_PLAIN));
            String greeting = this.service.callRestService();
            assertThat(greeting).isEqualTo("hello");
        }
    }

    (4). 除了上面几个类型, 还有 @MybatisTest 、 @JsonTest 、 @DataRedisTest 等等.

    =====================
    MockMvc 基本使用
    =====================
    MockMvc bean, 使用该对象的 perform() 可以发出模拟web请求, 并完成期望检查, 当然期望检查还可以使用JUnit 的 Assert. 虽然 MockMvc 是一个模拟的Http调用, 但它确实真实调用了视图函数, 并以 http 协议封装了结果, 所以可以用来做单元测试.

    @RestController
    @RequestMapping("/")
    class MyController {
    
        @GetMapping("/")
        public String index() throws SQLException {
            return "index";
        }
    } 
    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class MyControllerTests {
    
        @Autowired
        private MockMvc mvc;
    
        /**
         * 期望调用成功, 并打印完整结果.
         *
         * @throws Exception
         */
        @Test
        public void indexPrint() throws Exception {
    
            //@formatter:off
            mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(MockMvcResultHandlers.print());
            //@formatter:on
        }
    
        /**
         * 期望调用成功, 并验证该视图函数的返回内容是否为字符串 index
         *
         * @throws Exception
         */
        @Test
        public void index() throws Exception {
            String expected = "index";
            //@formatter:off
            mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andExpect(MockMvcResultMatchers.content().string(expected));
            //@formatter:on
        }
    }


    =====================
    Mockito基本使用
    =====================
    顶层class经常会依赖一些底层class, 要对顶层class做单元测试, 就需要使用 mock 技术来代替底层class.
    1. @MockBean 注解, 该 Mockito 注解可注入顶层对象, 它和 @Autowired 用法含义差不多, 但并不会注入真实的底层实现类.
    2. 使用 Mockito.when() 来模拟底层类的行为:
    Mockito.when(methodCall).thenReturn(expected) 使用穷举法来模拟, 这个方式简单但最常用, 我们写测试用例基本上也是按照case by case 设计有限的几个测试用例.
    Mockito.when(methodCall).thenAnswer(匿名对象) 通过when()传入的参数动态模拟, 该方式很强大, 但并不常用, 因为我们在测试用例中, 没有必要再和被模拟对象一样实现一套业务逻辑.

    下面是被测试的代码,  MyServiceConsumer 依赖一个 MyService 接口. 

    interface MyService {
        public String appendA(String source);
    }
    
    @Service
    class DefaultMyService implements MyService {
    
        @Override
        public String appendA(String source) {
            System.out.println("MyServiceConsumer.appendA() called");
            return source + "B";
        }
    }
    
    @Component
    class MyServiceConsumer {
        @Autowired
        MyService myService;
    
        public String getServiceName(String source) {
            return myService.appendA(source);
        }
    }

    下面是MyServiceConsumer 的测试代码, 我们使用mockito 模拟了一个依赖 MyService 对象. 

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class MyServiceConsumerTests {
        @MockBean
        MyService myService;
    
        @Autowired
        MyServiceConsumer myServiceConsumer;
    
        /*
         * 使用 MockBean 来模拟 MyService 的 appendA() 行为
         */
        @Before
        public void Init() {
            // 方式1: 使用穷举法模拟, 具体是通过 thenReturn()方法
            String source = "MyService1";
            Mockito.when(myService.appendA(source))
                    .thenReturn(source + "A");
    
            // 方式2:使用动态参数形式模拟, 具体是通过 thenAnswer()方法
            Mockito.when(myService.appendA(Mockito.anyString()))
                    .thenAnswer(new Answer<String>() {
    
                        @Override
                        public String answer(InvocationOnMock invocation) throws Throwable {
                            String arg = (String) invocation.getArgument(0);
                            return arg + "A";
                        }
                    });
        }
    
        @Test
        public void getServiceName1() {
            String source = "MyService1";
            String expected = "MyService1A";
            String actual = myServiceConsumer.getServiceName(source);
            Assert.assertEquals("错误: getServiceName() 不符合预期", expected, actual);
        }
    
        @Test
        public void getServiceName2() {
            String source = "Other";
            String expected = "OtherA";
            String actual = myServiceConsumer.getServiceName(source);
            Assert.assertEquals("错误: getServiceName() 不符合预期", expected, actual);
        }
    }

    =====================
    参考
    =====================
    使用@SpringBootTest注解进行单元测试
    https://www.cnblogs.com/ywjy/p/9997412.html
    Testing in Spring Boot
    https://www.baeldung.com/spring-boot-testing
    第三十五章:SpringBoot与单元测试的小秘密
    https://segmentfault.com/a/1190000011420910
    http://tengj.top/2017/12/28/springboot12/
    springboot(十二):springboot如何测试打包部署
    http://www.ityouknow.com/springboot/2017/05/09/springboot-deploy.html

  • 相关阅读:
    CLR via C#深解笔记三
    CLR via C#深解笔记二
    CLR via C#深解笔记一
    C#参考:Linq 概述
    JavaScript
    jQuery
    JavaScript
    云原生
    python模块----optparse模块、argparse模块 (命令行解析模块)
    python模块----pymysql模块 (连接MySQL数据库)
  • 原文地址:https://www.cnblogs.com/harrychinese/p/springboot_unittesting2.html
Copyright © 2011-2022 走看看