架构系列十八(微服务测试设计实践思考)

194 阅读5分钟

关于测试这个话题,小伙伴们都很熟悉,在具体展开前,我们先来看一个图(关于经典软件工程阶段,与互联网软件交付阶段)

image.png

在经典软件工程阶段中,有编码测试阶段,说明了测试的重要性;

在互联网软件交付阶段中,没有直接看到测试相关的字眼,但是有生产就绪阶段,下面我们把生产就绪展开来看

image.png

在生产就绪checklist中,我们看到

  • 功能测试ok
  • 性能测试ok

不言而喻说明了测试的重要性。尤其在云原生微服务架构体系实践中,实践好CI/CD、DevOps的前置条件,测试至关重要!

那么今天,顺接上一篇分享文章:云原生微服务架构体系实践思考,与你共同探讨关于微服务测试设计的一些思考,并期望在生产实践中带来一些帮助。

1.测试金字塔

当我们谈到测试的时候,传统意义上我们都知道软件测试需要

  • 单元测试
  • 集成测试
  • 用户测试

但是细分的话,估计很多小伙伴不一定能明确它们之间的边界。下面我们从应用分层架构开始谈起,一个基于mvc分层架构应用,大概是这样的

image.png

上图是典型的三层应用架构

  • controller
  • service
  • dao

同时还涉及到服务之间调用,以及引用外部存储服务。我们需要考虑服务内部各层、服务之间、外部依赖服务以至于整个链路的测试ok。于是业界提出了测试金字塔的理论模型,何为测试金字塔呢?看一个图

image.png

我们看到整个测试阶段分为

  • 单元测试
  • 集成测试
  • 组件测试
  • 端到端测试
  • 探索测试

单元测试比较好理解,比如说针对每个controller、service、dao类及方法的测试

集成测试与组件测试,在实践中可以作为一个阶段,比如开发好某个功能后,通过postman工具调用测试(前后端联调)

端到端测试,即测试小伙伴,产品、用户进行验证测试了

探索测试,即构建一些自动化的测试(比较高级)

2.构建单元测试案例

理解测试金字塔体系以后,最重要的需要在实际项目中实践起来,才显得有意义!精通的目的全在于应用!

下面我们以一个springboot应用为例,看如何构建相关的单元测试用例。这个对于研发小伙伴来说很重要,我们在开发好每个功能每个类,原则上都应该要配备相关的单元测试类,如此能为我们的交付质量保驾护航!

2.1.整体应用结构

image.png

image.png

2.2.关键环境准备说明

基于springboot的应用,实践单元测试非常容易,环境方面准备

  • 引入:spring-boot-starter-test依赖
  • 注解:@RunWith
  • 注解:@SpringBootTest

2.2.1.引入依赖

引入test依赖的同时,引入了h2数据库依赖,h2是内存数据库,用于解除执行单元测试用例时,对外部存储mysql的依赖

<!--test 依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!--内存数据库h2依赖-->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

2.2.2.测试基类BaseTester

/**
 * 测试基类
 *  构建命令:mvn clean package
 *  跳过单元测试:
 *      mvn clean package -DskipTests
 *      mvn clean package -Dmaven.test.skip=true
 * @author ThinkPad
 * @version 1.0
 * @date 2021/10/4 16:45
 */
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class BaseTester {
​
}

2.2.3.Web测试基类WebBaseTester

执行controller层单元测试用例,需要相关的一些mock,解除对外部服务的依赖,注意类上面的webEnvironment配置,以及@AutoConfigureMockMvc注解

/**
 * web测试基类
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/10/4 16:55
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@AutoConfigureMockMvc
public class WebBaseTester extends BaseTester{
​
}

2.2.4.编写测试专用配置

为了解除对外部服务的依赖,或者外部环境的依赖,比如

  • 外部存储mysql,通过内存数据库h2

因此,需要准备特定的测试配置文件,下面关注数据源datasouce配置

spring:
  application:
    name: follow-me-springboot-test
  datasource:
    url: jdbc:h2:mem:account;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MYSQL
    username: sa
    password:
    driver-class-name: org.h2.Driver
    continue-on-error: false
    platform: h2
    schema: classpath:/db/schema.sql
  h2:
    console:
      enabled: true
  jpa:
    hibernate:
    ddl-auto: validate
    show-sql: true
    properties:
      hibernate:
        format_sql: true
#feign配置
feign:
  client:
    config:
      default:
        loggerLevel: basic
#日志配置
logging:
  level:
    cn.edu.anan: debug

2.3.相关测试用例

2.3.1.controller单元测试用例

/**
 * controller层测试类
 * mockito框架使用参考:https://segmentfault.com/a/1190000006746409
 * @author ThinkPad
 * @version 1.0
 * @date 2021/10/3 16:52
 */
@Slf4j
public class BookControllerTest extends WebBaseTester{
​
    @Autowired
    MockMvc mockMvc;
​
    @MockBean
    private AccountClient accountClient;
​
    /**
     * 初始化测试数据
     */
    @Autowired
    private BookController bookController;
​
    @Before
    public void setUp(){
        Book book = Book.builder()
                .id(1)
                .author("xy")
                .bookName("java")
                .publishDate(LocalDate.now())
                .build();
        Book book2 = Book.builder()
                .id(2)
                .author("lj")
                .bookName("spring")
                .publishDate(LocalDate.now())
                .build();
​
        bookController.save(book);
        bookController.save(book2);
    }
​
    /**
     * 测试查询图书列表
     * @throws Exception
     */
    @Test
    public void testListBook() throws Exception{
        // 查询结果
        Account account = Account.builder()
                .id("666")
                .name("xy")
                .email("xy@126.com")
                .build();
        // mock
        when(accountClient.findById(anyString()))
                .thenReturn(account);
​
        // mock查询图书列表
        MvcResult mvcResult = mockMvc.perform(get("/book/list"))
                .andExpect(status().isOk())
                .andReturn();
​
        String contentAsString = mvcResult.getResponse().getContentAsString();
        log.info("测试结果:{}", contentAsString);
​
    }
}

2.3.2.service单元测试用例

/**
 * service层测试
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/10/3 16:12
 */
@Slf4j
public class AccountServiceTest extends BaseTester{
​
    @Autowired
    private AccountService accountService;
​
    @Test
    public void testService(){
        // 准备数据
        Account account = Account.builder()
                .name("yhh")
                .email("yhh@126.com")
                .build();
        // 保存
        accountService.save(account);
​
        // 查询
        Account accountById = accountService.findAccountById(account.getId());
        log.info("查询到数据:{}", accountById);
​
    }
​
}

2.3.3.dao单元测试用例

/**
 * dao层单元测试
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/10/3 15:41
 */
@Slf4j
@FixMethodOrder(value = MethodSorters.NAME_ASCENDING)
public class AccountDaoTest extends BaseTester{
​
    @Autowired
    private AccountDao accountDao;
​
    private Account account;
​
    /**
     * 初始化数据
     */
    @Before
    public void setUp() {
        log.info(".................setUp..................");
        // 准备账户数据
        account = Account.builder()
                .name("yhh")
                .email("yhh@126.com")
                .build();
​
    }
​
    /**
     * 保存数据
     */
    @Test
    public void test001Save(){
        log.info(".................test001Save..................");
        accountDao.save(account);
    }
​
    /**
     * 查询列表数据
     */
    @Test
    public void test002ListAccount(){
        log.info(".................test002ListAccount..................");
        // 查询
        Iterable<Account> all = accountDao.findAll();
        all.forEach(acc ->{
            log.info("查询到数据:{}", acc);
        });
​
    }
​
}

写在最后,有了上面的单元测试用例

  • 当我们在构建项目的时候,会选择执行相关的单元测试用例,只有测试用例都执行成功,才能完成构建
  • 或者配合CI/CD流水线,只有单元测试用例都执行通过的时候,才允许将代码合并到主干master分支,进一步构建完成发布部署

这即是我们今天这篇文章分享最重要的一个点,另外本文完整源码需要的小伙伴,可以参考代码仓库:gitee.com/yanghouhua/…,其中的follow-me-springboot-test模块