案例分享

西魏陶渊明 ... 2022-3-24 大约 9 分钟

# 前言

单测覆盖率为应用质量指标化提供解决方案

一口吃不了一个胖子, 由于每个人的认识不一样,要想达到理想的状态,显然是不可能的。此章节主要从实践出发,一步一步带领我们完成一个有效的可复用的单元测试的编写。

# 一、目标

# 1.1 质量保障

单测是保证项目质量的手段,而不是目的。

通过 mvn test 在每次编译时候,对代码进行测试。以到达可持续集成的目的。这里的关键是在每次服务发布的时候,首先要运行测试用例。 只有这样,我们的单测用例才有价值,才能提前发现问题。

# 1.2 打扫屋子

要解决前面的这个问题,就要求我们编写正确的测试用例。因为大多数项目目前的编译脚本都是,跳过单元测试的. mvn -Dmaven.test.skip=true, 之所以这样是因为大多数的单测都是为 debug 写的,一旦在编译的时候执行单测用例, 就会编译不通过。所以要想达到可持续集成的目的, 就要先把阻塞应用的单元测试用例给移除。

我们可以通过 @Ignore 来。 当然这不是说不允许你写debug的单测,而是要我们 遵守一个约定, 对那种debug的单测, 必须人工执行的单测 我们要通过 @Ignore 来标记, 避免我们执行单测时候失败。

# 1.3 发挥价值

如果做到了 1.1 和 1.2 那么单测的价值有会真正体现出来了。但是这一切的一切的前提是我们要编写出正确的符合规范的单测用例。

# 二、真实案例分享

首先这里我们使用到的技术,其实再前面的技术框架中都有列举了,所以这里就不详细说明了。这里就分享几个测试用例。

# 2.1 查询接口测试用例 难度:⭐️

对于上图这种仅仅涉及到读的接口,是比较简单的。我们的验证点其实只有两个。

  1. web接口是否可以调用
  2. 接口查询条件是否有效

# 2.1.1 启动web服务器

随机web端口,执行单测的时候启动一个web容器,用来模拟Web接口测试。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = CenterProviderApplication.class,
                webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 指定启动类
public class BaseApplicationTest {

    protected URL base;

    @LocalServerPort
    private int port;

    @Before
    public void setUp() throws Exception {
        this.base = new URL("http://localhost:" + port + "/pms/");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 2.1.2 编写接口

这里就验证了Web的请求,同时可以进度查询条件的校验。当然这里也可以加上返回值类型的校验

public class CommonControllerTest extends BaseApplicationTest {

    @Resource
    private CommonController commonController;

    /**
     * 使用测试工具进行web端测试
     *
     */
    @Test
    public void goodsList() {
        GoodsListDTO dto = new GoodsListDTO();
        dto.setSize(5L);
        dto.setCurrent(1L);
        // 使用该方法允许数据Mock
        JsonResult<DiyPage<PmsGoodsVO>> result = TestWebUtils.web(this.base).mockWhen(commonController).goodsList(dto);
        // 非空判断
        Assert.assertNotNull(result);
        // 接口响应值判断
        Assert.assertTrue(result.getMsg(), result.isSuccess());
        // 接口数据输出
        System.out.println(TestConsole.color("执行结果:"));
        System.out.println(TestConsole.colors(result, AnsiColor.BRIGHT_BLUE));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

看到这里我们能亲身感受到,对于纯粹读的接口测试用例是比较容易编写的。一个项目只用维护一次就好了,开发同学也不会每次都去维护这个用例。 虽然这个用例简单,但是我们能在每次发布完成后自动化的去执行校验。能帮助我们提前发现问题。

# 2.2 读写测试用例 难度:⭐️⭐️

像这种读写操作,且涉及到对外部数据调用的接口。我们要使用Mockito技术,对外部对象进行代理,并制定其中的行为。 如下图这个测试用例,看下如何编写测试用例。

首先分析下涉及到的接口。

  • saveArrivalOrder 这个接口里面是包含了内部的业务逻辑和外部的数据调用,包含了下面两个外部的接口依赖。
  1. noticeTransportNodeNote 在保存到货通知单的时候,通知库存增加商品的在途库存数量的
  2. syncPurchaseOrder 在保存到货通知单的时候,是通知仓储做到货准备的

这个业务中我们只需要验证我们内部的逻辑即可,涉及到外部的调用只需要验证代码即可,并不真的需要仓库和库存系统做 真实的业务处理。所以我们就要针对这两个方法做数据mock我们使用到的技术就是Mockito做对象行为的mock。如下案例。

主要看注释,我们在调用接口的时候,先通过给外部接口最Mockito代理,然后指定动作的出入参数。

public class NoticeOrderControllerTest extends BaseApplicationTest {

    @Autowired
    NoticeOrderController controller;

    /**
     * 接口穿插在po单创建的单侧里面
     */
    @Test
    public void saveArrivalOrder() {
        // 1. 构建保存单据的参数(从浏览器中复制数据)
        String saveOrderJson =  "{}"
        NoticeOrderDTO noticeOrder = TestConsole.toObject(saveOrderJson, ArrivalNoticeOrderDTO.class);

        // 2. mock库存中心返回 - inventoryTransportClient.noticeTransportNodeNote
        JsonResult<Boolean> noticeResult = JsonResult.success(true);
        Mockito.doReturn(noticeResult).when(inventoryTransportClient).noticeTransportNodeNote(Mockito.any());
        // 3. mock同步中控台 - bookingOrderPmsFeignClient.syncPurchaseOrder(bookingOrderDetailDTO)
        JsonResult<Object> syncResult = JsonResult.success();
        Mockito.doReturn(syncResult).when(bookingOrderPmsFeignClient).syncPurchaseOrder(Mockito.any());

        // 4. 执行保存+提交(走网络请求,事务不会自动回滚,如果想自动回滚直接调用,controller.saveArrivalOrder(arrivalNoticeOrder))
        arrivalNoticeOrder.setStatus(1);
        JsonResult<Long> longJsonResult = TestWebUtils.web(this.base).when(ArrivalNoticeOrderController.class)
                .saveArrivalOrder(arrivalNoticeOrder);
        PmsAssert.assertSuccess(longJsonResult);

        // 5. 验证关闭(这里因为我要验证关闭接口,所以不要自动会馆)
        Mockito.doReturn(syncResult).when(bookingOrderPmsFeignClient).cancelPurchaseOrder(Mockito.any());
        Mockito.doReturn(noticeResult).when(inventoryTransportClient).cancelTransportNode(Mockito.any());

        JsonResult<Void> jsonResult = TestWebUtils.web(this.base).when(ArrivalNoticeOrderController.class)
                .closeOrder((longJsonResult.getData()));
        PmsAssert.assertSuccess(jsonResult);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

这个案例,我们可以学会如何使用Mockito做外部接口的代理,整体难度也不是很大。

# 2.3 读写接口(复杂接口) 难度:⭐️⭐️⭐️⭐️

针对项目中核心流程, 可能并不仅仅是一个接口,而是有依赖关系的多个接口。

针对系统核心链路流程,我们是有必要维护些自动化流程的测试用例。然后对每个接口中的出入参数进行校验。**当做到这一步的时候 其实我们在发布时候就会更加有底气。敢于去重构代码了。**因为在项目迭代发布的过程中,我们会先跑一边单测。只要单测流程能通过,说明这个核心链路的流程问题就不会太大。

当然这里不是说一定没有问题, 而是有问题的几率变小了。如果说出现问题了,我们的自动化流程没有提前发现,那么说明这些场景,我们的单测流程没有覆盖到, 那么这个时候我们再补充我们的自动化流程。

注意这里的自动化流程,跟测试同学的自动化流程是有区别的。开发的自动化流程的测试目标是自身项目的代码,所以我们关于外部接口都是通过Mock来模拟的。而测试同学的 自动化流程,都是走真实的系统调用。

# 2.3.1 业务说明

  1. 创建一个包含一个商品的,草稿状态的临时采购单【验证保存接口】

  2. 模拟用户对采购单的商品进行二次修改【验证修改逻辑接口】

  3. 模拟对已经审核的采购单,添加不同类型货通知单【验证不同单据到货通知单的校验接口】

  4. 对已经创建的到货通知单进行人工关闭【验证关闭】

  5. 关闭所有到货通知单【验证关闭】

  6. 关闭采购单【验证采购关闭】

做到业务流程闭环

    @Test
    public void tempPurchaseOrderSave() {
        String saveOrderJson = "{\"poOrderItemList\":[{\"id\":null,\"goodsId\":\"1\",\"skuId\":\"1001062\",\"goodsName\":\"ABM品牌招商手册包11112\",\"englishName\":\"1\",\"goodsCode\":\"9314807022860\",\"brandCode\":\"brand230\",\"brandName\":\"34 西班牙\",\"specificationModel\":\"12ml/瓶\",\"wmsItemList\":[{\"purchaseNum\":null,\"warehouseName\":\"DHL\",\"warehouseCode\":0},{\"purchaseNum\":null,\"warehouseName\":\"PCA\",\"warehouseCode\":1},{\"purchaseNum\":null,\"warehouseName\":\"EWE\",\"warehouseCode\":2},{\"purchaseNum\":null,\"warehouseName\":\"新西兰仓\",\"warehouseCode\":3}],\"purchaseNum\":30,\"goodProductNum\":30,\"purchasePrice\":\"10\",\"expectDeliveryDate\":\"2021-05-12\",\"actualPurchaseNum\":null}],\"tradeCompanyIdList\":[\"13\"],\"supplierId\":\"347\",\"businessLineId\":\"1\",\"sameChainId\":49,\"title\":\"mevan test自动跑验证流程\",\"companyId\":null,\"channelCode\":\"ABM\",\"brandCode\":\"brand230\",\"purchaseBrandId\":\"1374262965415165953\",\"currencyCode\":\"AUD\",\"tradeType\":20,\"deliveryWarehouseId\":null,\"transferWarehouseId\":\"542\",\"salesWarehouseId\":\"9\",\"giveawayOrderSign\":1,\"ids\":\"\"}";
        PurchaseOrderDTO purchaseOrderDTO = TestConsole.toObject(saveOrderJson, PurchaseOrderDTO.class);
        purchaseOrderDTO.setTitle("mvn test(临时订单自动化流程验证)");
        List<PurchaseOrderDetailDTO> poOrderItemList = purchaseOrderDTO.getPoOrderItemList();
        for (PurchaseOrderDetailDTO purchaseOrderDetailDTO : poOrderItemList) {
            // 赠品刚开始设置成1
            purchaseOrderDetailDTO.setGiveawayNum(1);
            purchaseOrderDetailDTO.setExpectDeliveryDate(JodaTimeUtils.addDay(new Date(), 1));
        }
        // 1. 执行保存
        Long purchaseOrderId = createPurchaseOrder(purchaseOrderDTO);

        // 2. 然后修改下商品明细
        List<PurchaseOrderDetailDTO> updateOrderDetailList = purchaseOrderDTO.getPoOrderItemList();
        EnhanceStream.findAny(updateOrderDetailList)
                .ifPresent(order -> {
                    order.setGiveawayNum(0);
                });
        purchaseOrderDTO.setId(purchaseOrderId);//第二次要修改要orderType
        purchaseOrderDTO.setOrderType(PurchaseOrderTypeEnum.TEMPORARY_ORDER.getKey());
        updatePurchaseOrder(purchaseOrderDTO);

        PurchaseOrderDTO audit = new PurchaseOrderDTO();
        audit.setId(purchaseOrderId);
        audit.setSameChainId(purchaseOrderDTO.getSameChainId());
        audit.setTradeCompanyIdList((purchaseOrderDTO.getTradeCompanyIdList()));
        audit.setSupplierId(purchaseOrderDTO.getSupplierId());
        audit.setCompanyId(purchaseOrderDTO.getCompanyId());

        // 2. 执行审核
        JsonResult<Void> auditJson = TestWebUtils.web(this.base).when(PurchaseOrderController.class).audit(audit);
        PmsAssert.assertSuccess(auditJson);

        // 3. 订单查询
        PurchaseOrderVO purchaseOrderDetail = queryPurchaseOrderDetail(purchaseOrderId);

        // 3.1 对po单主单里面的必填值
        assertPo(purchaseOrderDetail, false, purchaseOrderDetail);

        // 4. 生成三笔到货通知单,然后关闭
        createArrivalOrderAndClose(purchaseOrderId);

        // 5. PO订单手动关闭
        closePurchaseOrder(purchaseOrderId, "maven test 自动化脚本");

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

当执行完上面的单测用例,就完成了采购核心链路的自动化流程,就会生成下面单据。

采购单

到货通知单

# 三、可复用能力

通过2我们知道如何使用Mockito做对象行为的mock。还有一个点要思考的,这里单独拿出来说。就是测试用例的可复用行。什么叫可复用性呢? 就是说你这个单侧用例,在每次代码提交和编译时候执行,是否会有问题。如果是,且只能运行一次,那么这个就是不合格的。要做到可复用还要考虑两个地方。

# 3.1 接口参数动态生成

为什么要动态生成呢? 因为比如我们指定了一个id去作为查询条件,那么当我们这个id被删除的时候,这个测试用例一定是失败的。 这种场景下我们就应该动态去查询一个。如下,我们查询采购单的全链路数据。那么首先我先查询所有的有效的采购单,然后拿出任意一条 作为测试用例的查询条件即可。

    @Test
    public void queryFullLinkData() {
        // 执行测试用例时候,动态获取一个采购单code
        String orderCode = testOrderUtils.purchaseOrderCode();
        TrackFullLinkDTO trackFullLinkDTO = new TrackFullLinkDTO();
        trackFullLinkDTO.setCurrent(1L);
        trackFullLinkDTO.setSize(10L);
        trackFullLinkDTO.setPurchaseOrderCode(orderCode);
        JsonResult<TrackFullLinkVO> fullLinkData = TestWebUtils.web(this.base).when(PurchaseOrderController.class).queryFullLinkData(trackFullLinkDTO);
        PmsAssert.assertSuccess(fullLinkData);
    }
1
2
3
4
5
6
7
8
9
10
11

上面这个用例只用声明一个测试的bean对象即可。在我们的 src/test/java包中。

TestComponent

@TestComponent
public class TestOrderUtils {

    @Resource
    private ArrivalNoticeOrderController arrivalNoticeOrderController;

    public String purchaseOrderCode() {
        return purchaseOrder().getOrderCode();
    }
1
2
3
4
5
6
7
8
9

# 3.2 业务流程要闭环

  1. 要么数据执行完成自动回滚
  2. 要么数据不回滚,但是业务流程要闭环。

本文由西魏陶渊明版权所有。如若转载,请注明出处:西魏陶渊明
上次编辑于: 2022年6月16日 21:10
贡献者: lxchinesszz