前言

我们有很多篇幅介绍了如果使用mock挡板等工具做无外部依赖单元测试,本章聊一下单元测试中的断言和校验器。

断言Assertj

推荐用Assertj 断言,反观Assert不是那么的直观好用。Assertj支持字符串、数字、日期、List、Map、Class等类型,此外还提供了好用的 fail 方法。除此之外,对Java中的Exception、Iterable、JodaTime、Guava等都提供支持。

    import static org.assertj.core.api.Assertions.*;

    @Test
    public void testList() {
        List<String> names = Lists.newArrayList("zhangsan", "lisi", "wangwu", "zhaoliu");
        // assertj 3 需要加 asList() 你可以一起校验也可以分开校验
        assertThat(names).asList().hasSize(4);
        // 是否有4个元素、开始的第一个是zhangsan、最后一个是zhaoliu、其中有lisi、只有一个wangwu
        assertThat(names).asList()
                .hasSize(4).startsWith("zhangsan").endsWith("zhaoliu")
                .contains("lisi", atIndex(1))
                .containsOnlyOnce("wangwu");
    }

    @Test
    public void testMap() {
        Map<String, String> map = Maps.newHashMap();
        map.put("a", "A");
        map.put("b", "B");
        map.put("c", "C");

        //提取extracting(keys)中的值,是否包含一个A值、是否没有包含D值
        assertThat(map).extracting("a", "b", "c").contains("A").doesNotContain("D");
        //map满足satisfies 一个b的key、b中的值是B
        assertThat(map).satisfies(s -> s.containsKey("b")).extracting("b").contains("B");
    }

    @Test
    public void testClass() {
        // 断言 没有注解类
        assertThat(PersonInfo.class).isNotAnnotation();
        // 断言 有注解类
        assertThat(Deprecated.class).isAnnotation();
        // 断言 存在注解为@Deprecated
        assertThat(PersonInfo.class).hasAnnotation(Deprecated.class);
        // 断言 不是接口
        assertThat(PersonInfo.class).isNotInterface();
        // 断言 Object 类是 PersonInfo 类的父类
        assertThat(Object.class).isAssignableFrom(PersonInfo.class);
    }

    @Test
    public void testContent() {
      	//一般用法就是比较内容的:assertThat(比较的内容).as(失败时的说明).xxx(结果)
      	boolean flag = true;
        assertThat(flag).as("flag is true.").isEqualTo(true);
    }

    //异常情况请看第七章有很多例子。
    @Test
    public void testException() {
        assertThatExceptionOfType(IOException.class).isThrownBy(() -> { throw new IOException("xxx!"); })
                .withMessage("xxx!")
                .withMessageContaining("xx")
                .withMessage("%s!", "xxx")
                .withStackTraceContaining("IOException")
                .withNoCause();
    }

    @Test
    public void testFail() {
      	//fail相当于抛异常,抛AssertionError的异常
        try {
            fail("在不检查任何条件的情况下使断言失败。显示一则消息");
        } catch (AssertionError ae) {
            System.out.println(ae);// 会有输出
        }
        try {
            failBecauseExceptionWasNotThrown(RuntimeException.class);
        } catch (AssertionError ae) {
            System.out.println(ae);// 会有输出
        }
    }

校验器Verify

校验器mockito和powerMockito合用

被测试的方法

    @Resource
    private ArchTopologyNebulaDAO archTopologyNebulaDAO;

    @Resource
    private SkyWalkingManager skyWalkingManager;
    @Override
    public boolean execute(List<String> list) {
        if (!CollectionUtils.isEmpty(list)) {
            for (String str : list) {
              	//验证点3
                SwTopologyDTO data = skyWalkingManager.getServiceTopology(str);
                if (data != null) {
                    Set<String> nGqlsByDatabases = Sets.newHashSet();
                  	//验证点1
                    Map<String, String> nebulaDatabases = archTopologyNebulaDAO.query2MapBy("id", "type", str);
                    Map<String, SwTopologyDTO.Node> swNodeMap = Maps.newHashMap();
                    for (SwTopologyDTO.Node n : data.getNodes()) {
                        nGqlsByDatabases.add(n.getNodeId + str);
                    }
                  	//验证点2
                    archTopologyNebulaDAO.save2nebula(nGqlsByDatabases);
                }
            }
        }
        return true;
    }

测试这么写

    @Test
    public void selectExecuteTest() throws NoSuchFieldException {
      	List<String> data = List<String> data = Lists.newArrayList("probe-app", "warehouse-app");

        boolean flag = archTopologyService.execute(data);
        //校验是否被调用过2次
        verify(archTopologyNebulaDAO, times(2)).query2MapBy(any(), any(), any());
        verify(archTopologyNebulaDAO, times(2)).save2nebula(anyCollection());
				
      	//验证调用顺序
      	InOrder inOrder = inOrder(skyWalkingManager, archTopologyNebulaDAO);
        inOrder.verify(skyWalkingManager).getServiceTopology("probe-app");
        inOrder.verify(skyWalkingManager).getServiceTopology("warehouse-app");
      	
        assertThat(flag).as("flag is true.").isEqualTo(true);
    }

参数匹配器

在某些场景中,不光要对方法的返回值和调用进行验证,同时需要验证一系列交互后所传入方法的参数。那么我们可以用参数捕获器来捕获传入方法的参数进行验证,看它是否符合我们的要求。

    argument.capture() 捕获方法参数
    argument.getValue() 获取方法参数值,如果方法进行了多次调用,它将返回最后一个参数值
    argument.getAllValues() 方法进行多次调用后,返回多个参数值


    @Test
    public void selectExecuteTest() throws NoSuchFieldException {
      	List<String> data = List<String> data = Lists.newArrayList("probe-app", "warehouse-app");

        boolean flag = archTopologyService.execute(data);
  			
        //验证save2nebula这个方法被调用4次,最后一次的参数个数是1
        ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
        verify(archTopologyNebulaDAO, times(4)).save2nebula(argument.capture());
  			assertThat(argument.getValue().size()).as("size is 1").isEqualTo(1);
        assertThat(argument.getAllValues().size()).as("size is 4").isEqualTo(4);
    }

额外说明

//any和anyString这种是有区别的,any代表包含null,anyString是不包含空的。如果是数组就这么用,比如
String write(byte[] fileByte)
any(byte[].class)
  
String doSomeThing(List<T> list)
any(List.class)

静态方法验证

静态方法验证需要用到PowerMockito,单单Mockito是无法满足的。而且静态方法测试有特殊的写法需要注意。

比如我在写sentinel的时候要用到里面的静态类帮我做拦截,我想知道ContextUtil.enter这个静态方法被调用了多少次,该怎么做

    public boolean flowControl(EventBus eventBus) throws BlockException {
        if (!sentinelSwitch) {
            return handle(eventBus);
        }
        Entry entry = null;
        try {
          	//测试点
            ContextUtil.enter(sentinelResourceName());
            entry = SphU.entry(sentinelResourceName(), EntryType.IN);
            return handle(eventBus);
        } catch (BlockException ex) {
            throw ex;
        } finally {
            if (entry != null) {
                entry.exit();
            }
            ContextUtil.exit();
        }
    }

    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messageExtList) {
      	for (MessageExt msg : messageExtList) {
        		reconsumeTimes = msg.getReconsumeTimes();
        		String body = new String(msg.getBody());
        		EventBus eventBus = JSON.parseObject(body, EventBus.class);
        		boolean result = flowControl(eventBus);
      	}
    }

测试这么写

    @Test
    public void consumeMessageNormalTest() throws NoSuchFieldException {
      	//静态mock在《第六章》也做了示例,请到里面查看。
        PowerMockito.mockStatic(ContextUtil.class);

        // 测试消费消息逻辑,模拟被调用了flowControl被调用了10次
        MessageExt msg = new MessageExt();
        msg.setMsgId("123");
        msg.setBody("{}".getBytes());

        for (int i = 0; i < 10; i++) {
            concurrently.consumeMessage(Lists.newArrayList(msg), null);
        }
				
      	//这里用PowerMockito的verifyStatic来校验
        PowerMockito.verifyStatic(ContextUtil.class, times(10));
        //重要:告知最后要verifyStatic验证的静态方法是哪一个,否则异常
        ContextUtil.enter(anyString());
				
    }

总结

基本上已经做了非常多常见场景的示例,学完已经能解决绝大部分的单元测试,单元测试的很多方法很多用法我们可以在使用中去发现,后期如果有更多好玩的方法我也会再写文章分享出来。

上一篇 下一篇