很多小伙伴所在的公司是基于Dubbo来构建技术栈的,日常开发中必不可少要写dubbo单测(单元测试),如果单测数据依赖已有的外部dubbo服务,一般是mock数据,如果数据比较复杂,其实mock数据也是一个不小的工作量。那有没有更好的单测方式来代替我们完成”mock“数据功能呢,这时可以借助dubbo telnet功能,获取真实数据用在单测中使用。

本文会先讨论如何使用基于dubbo telnet的代理工具类(DubboTelnetProxy),然后再讨论下mockito+DubboTelnetProxy如何进行多层次的单测,最后分析下如何让单测变得更加智能(比如自动注入等)。(ps:关于dubbo和mockito这里就不展开讨论了,具体可以参考对应资料~

1 Dubbo单测现状

dubbo单测其实和非dubbo单测的流程是一样的,初始化待测试类和单测上下文,打桩然后调用,最后检查返回结果。比如我们常用mockito来跑单测,其简单的示例如下:

public class DubboAppContextFilterTest extends BaseTest {
    private DubboAppContextFilter filter = new DubboAppContextFilter();     @Before
    public void setUp() {
        cleanUpAll();
    }     @After
    public void cleanUp() {
        cleanUpAll();
    }     @Test
    public void testInvokeApplicationKey() {
        Invoker invoker = mock(Invoker.class);
        Invocation invocation = mock(Invocation.class);
        URL url = URL.valueOf("test://test:111/test?application=serviceA");
        when(invoker.getUrl()).thenReturn(url);         filter.invoke(invoker, invocation);
        verify(invoker).invoke(invocation);         String application = RpcContext.getContext().getAttachment(DubboUtils.SENTINEL_DUBBO_APPLICATION_KEY);
        assertEquals("serviceA", application);
    }
}

上面代码copy于sentinel的单元测试代码。

2 DubboTelnetProxy

在dubbo服务机器上,我们可以使用telnet连接dubbo服务,然后执行invoke命令来手动调用dubbo接口并获取结果,DubboTelnetProxy就是将这一系列的手动操作按照dubbo telnet格式固化到代码中。在具体讨论DubboTelnetProxy之前,先看下其有哪些功能,DubboTelnetProxy特点:

  • 基于telnet的dubbo代理工具类,可用于本地单测中;
  • 直接使用telnet指定ip+port进行连接,无需更多的dubbo相关配置,使用便捷;
  • 可动态配置ip+port信息。

话不多说,先看下DubboTelnetProxy代码实现:

@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class DubboTelnetProxy implements MethodInterceptor {     private String ip;
    private Integer port;     @Override
    public Object intercept(Object obj, Method method, Object[] params, MethodProxy proxy) throws Throwable {
        if ("toString".equals(method.getName())) {
            return obj.getClass().getName();
        }         TelnetClient telnetClient = new TelnetClient();
        telnetClient.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(5));
        telnetClient.connect(ip, port);
        try {
            InputStream in = telnetClient.getInputStream();
            PrintStream out = new PrintStream(telnetClient.getOutputStream());             // 1. 发送dubbo telnet请求
            StringBuffer request = new StringBuffer("invoke ");
            request.append(method.getDeclaringClass().getTypeName()).append(".");
            request.append(method.getName()).append("(");
            request.append(StringUtils.join(Arrays.stream(params).map(JSON::toJSONString).collect(Collectors.toList()), ",")).append(")");
            out.println(request.toString());
            out.flush();             // 2. 结果处理
            int len = 0;
            byte[] buffer = new byte[512];
            String result = "";
            while (!result.contains(StringUtils.LF) && (len = in.read(buffer)) > 0) {
                result += new String(ArrayUtils.subarray(buffer, 0, len));
            }
            result = StringUtils.substringBefore(result, StringUtils.LF);
            if (StringUtils.isBlank(result) || !result.startsWith("{")) {
                throw new RuntimeException(result);
            }             // 3. 反序列化
            return JSON.parseObject(result, method.getGenericReturnType());
        } finally {
            telnetClient.disconnect();
        }
    }     /**
     * mockDubboIpPortFormat:配置格式为  -Dmock.dubbo.%s=127.0.0.1:8080,%s为当前dubbo接口的名字,class.getSimpleName()
     */
    private final static String mockDubboIpPortPrefix = "mock.dubbo.";
    public final static String mockDubboIpPortFormat = mockDubboIpPortPrefix + "%s";     /**
     * dubbo telnet建造者
     */
    public static class Builder {
        final static String DEFAULT_IP = "127.0.0.1";
        final static Integer DEFAULT_PORT = 20880;         /**
         * 创建dubbo telnet代理
         */
        public static <T> T enhance(Class<T> clazz) {
            return enhance(clazz, null, null);
        }
        public static <T> T enhance(Class<T> clazz, String ip) {
            return enhance(clazz, ip, null);
        }
        public static <T> T enhance(Class<T> clazz, Integer port) {
            return enhance(clazz, null, port);
        }         @SuppressWarnings("unchecked")
        public static <T> T enhance(Class<T> object, String ip, Integer port) {
            // 优先尝试从properties解析ip:port配置
            String ipPort = System.getProperties().getProperty(String.format(mockDubboIpPortFormat, object.getSimpleName()));
            if (StringUtils.isNotEmpty(ipPort)) {
                String[] array = StringUtils.split(ipPort, ",");
                ip = array[0];
                port = Integer.valueOf(array[1]);
            }             Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(object);
            enhancer.setCallback(new DubboTelnetProxy(ObjectUtils.defaultIfNull(ip, DEFAULT_IP), ObjectUtils.defaultIfNull(port, DEFAULT_PORT)));
            return (T) enhancer.create();
        }
    }
}

DubboTelnetProxy的实现原理是使用cglib生成dubbo facade接口代理类,然后在代理类按照dubbo telnet格式拼接请求参数,最后获取返回结果并反序列化返回给应用程序。上述代码不足点是:目前每次dubbo调用都会新建telnet连接,对于单测来说是OK的,后续如果用于本地压测或者调用频繁测试场景,考虑复用连接或者使用netty client bootstrap方式避免每次都新建连接。

2.1 使用示例

手动/自动指定dubbo服务IP地址:

@Test
public void test() {
  // OrderQueryService为dubbo服务的一个API接口
  System.setProperty("mock.dubbo.OrderQueryService", "127.0.0.1:20880");   OrderQueryService orderQueryService1 = DubboTelnetProxy.Builder.enhance(OrderQueryService.class);
  OrderQueryService orderQueryService2 = DubboTelnetProxy.Builder.enhance(OrderQueryService.class, "127.0.0.1");
  OrderQueryService orderQueryService3 = DubboTelnetProxy.Builder.enhance(OrderQueryService.class, "127.0.0.1", 20880);   OrderDTO result = orderQueryService1.query("订单号");
  System.out.println(result);
}

3 DubboTelnetProxy + mockito自动注入

日常开发中,可以使用mockito进行单测,保证代码质量。在mockito中,如果想让某个DubboTelnetProxy代理类注入到待测试中,可使用FieldUtils工具类进行属性注入。

使用DubboTelnetProxy + mockito示例如下:

@RunWith(MockitoJUnitRunner.class)
public class DemoServiceClientTest {
    @InjectMocks
    DemoServiceClient demoServiceClient;     @Before
    public void before() throws IllegalAccessException {
        FieldUtils.writeField(demoServiceClient, "demoServiceFacade",
                DubboTelnetProxy.Builder.enhance(DemoServiceFacade.class), true);
    }     @Test
    public void hello() throws IllegalAccessException {
        // 调用远程服务,DubboTelnetProxy方式
        demoServiceClient.hello("world");         // 如果需要打桩,则使用Mock类
        DemoServiceFacade demoServiceFacade = Mockito.mock(DemoServiceFacade.class);
        Mockito.when(demoServiceFacade.hello("world")).thenReturn("zzz");
        FieldUtils.writeField(demoServiceClient, "demoServiceFacade", demoServiceFacade, true);
        Assert.assertEquals(demoServiceClient.hello("world"), "zzz");
    }
} @Component
public class DemoServiceClient {
    @Resource
    private DemoServiceFacade demoServiceFacade;     public String hello(String world) {
        return demoServiceFacade.hello(world);
    }
} // dubbo api
public interface DemoServiceFacade {
    String hello(String world);
}

3.1 如何自动注入

要实现DubboTelnetProxy的自动注入,首先判断出来待测试类中的哪些属性需要构造DubboTelnetProxy或者对应实例,一般情况下如果属性是非本工程内的接口类型,就可以认为是dubbo api接口,进行构造DubboTelnetProxy并注入;如果属性是本工程内的接口类型,则在本工程内查找对应的实现类进行反射方式的属性注入(可使用org.reflections包中的Reflections工具类来获取接口下所有实现类);如果属性是普通类,则直接反射构建对象注入即可,伪代码如下:

/**
 * 默认的dubbo属性构造器,如果是非本工程内属性类型并且是接口类型,直接进行DubboTelnetProxy构建
 */
public static Function<Field, Object> DEFAULT_DUBBO_FC = field -> {
    try {
        assert Objects.nonNull(targetContext.get());
        Class fieldClass = field.getType();
        if (fieldClass.isInterface()) {
            // 本工程内的加载其实现类,非本工程内的按照DubboTelnetProxy构建
            if (!isSameProjectPath(targetContext.get().getClass(), fieldClass)) {
                return DubboTelnetProxy.Builder.enhance(fieldClass);
            } else if (fieldClass.getSimpleName().endsWith("Dao")) {
                return Mockito.mock(fieldClass);
            } else {
                String packagePath = fieldClass.getPackage().getName() + ".impl.";
                return Class.forName(packagePath + fieldClass.getSimpleName() + "Impl").newInstance();
            }
        } else if (isSameProjectPath(targetContext.get().getClass(), fieldClass)) {
            return fieldClass.newInstance();
        } else {
            // 非工程内的类直接mock掉
            return Mockito.mock(fieldClass);
        }
    } catch (Exception e) {
        System.err.println("DEFAULT_DUBBO_FC 发生异常 field=" + field);
        e.printStackTrace();
        System.exit(-1);
        return null;
    }
};

针对待注入类有多个层次,比如测试类A中属性b类型是B,B中属性c类型是C等,那么在自动注入类A的所有属性时,需要递归进行,直至所有子类型的属性都构建完毕,示例伪代码如下:

void doWithFieldsInternal(@NonNull Object target, @Nullable Function<Field, Object> fc, @Nullable Boolean recursive) {
    assert !(target instanceof Class);
    // 默认fc回调直接调用默认无参构造方法
    fc = ObjectUtils.defaultIfNull(fc, DEFAULT_FC);
    recursive = ObjectUtils.defaultIfNull(recursive, false);     List<Object> fieldList = new ArrayList<>();
    do {
        Object finalTarget = target;
        Function<Field, Object> finalFc = fc;
        ReflectionUtils.doWithFields(finalTarget.getClass(), field -> {
            Object value = finalFc.apply(field));
            DubboReflectionUtils.setField(finalTarget, field, value);
            if (Objects.nonNull(value) && DEFAULT_FF.matches(field)) {
                fieldList.add(value);
            }
        }, filterField -> {
            // 默认只注入非基本类型并且为null的属性
            return DEFAULT_FF.matches(filterField) && DubboReflectionUtils.isNullFieldValue(finalTarget, filterField);
        });
    } while (recursive && !fieldList.isEmpty() && Objects.nonNull(target = fieldList.remove(0)));
}

3.2 如何让自动注入更易用

上述示例中的自动注入是程序会递归注入待测试类中的所有属性,但还是需要在代码中先调用要"自动注入"的代码,为了更易用,可以使用注解方式来自动注入被注解修饰的所有类或者属性,类似于在Spring中对类属性配置了@Resource之后,Spring在容器启动过程中会自动对该属性注入对应示例,开发者无需关注。

关于如何实现mockito+DubboTelnetProxy的注解方式自动注入,笔者就不在赘述,感兴趣的小伙伴可以参考3.1中的实现思路自行实现。

说道注解,其实想实现针对某些注解执行一些特定逻辑(比如执行自动注入),可以在两种阶段对其处理,如下所示:

  • 编译处理阶段:比如设置Java的注解处理器,一般是继承AbstractProcessor来实现特定业务逻辑,其主要的处理逻辑就是扫描、评估和处理注解的代码,以及生产 Java 文件。比如lombok中的@Setter注解就是要产生对应属性的setter方法;
  • 容器启动阶段:这里的容器是业务程序自己定义的容器,比如Spring的IoC容器,在容器启动过程中针对注解进行处理,首先获取注解对应的属性,然后从容器中获取属性对应的实例通过反射将其注入即可。

以上两种自动注入方式在实现都是OK的,前者在编译阶段后者在运行时,不过后者由于在运行时起作用,因此灵活性更大。

 推荐阅读 

欢迎小伙伴关注【TopCoder】阅读更多精彩好文。

如何优雅地执行dubbo"单测"的更多相关文章

  1. 使用Groovy+Spock轻松写出更简洁的单测

    当无法避免做一件事时,那就让它变得更简单. 概述 单测是规范的软件开发流程中的必不可少的环节之一.再伟大的程序员也难以避免自己不犯错,不写出有BUG的程序.单测就是用来检测BUG的.Java阵营中,J ...

  2. Allure对单测结果以及robotframework结果的处理

    Allure对单测结果以及robotframework结果的处理 Allure只能针对pytest的单测结果生成相应的报告: 如果需要对unittest的测试框架结果进行展示,可以使用pytest执行 ...

  3. jmeter的dubbo压测,依赖jar包要放到执行机的lib/ext下

    对于jmeter的dubbo压测场景的master-slave结构: 即master的jmeter进行任务的下发和报告的生成,slave进行任务的执行 因为dubbo压测需要依赖很多三方jar包,那么 ...

  4. 【spock】单测竟然可以如此丝滑

    0. 为什么人人都讨厌写单测 在之前的关于swagger文章里提到过,程序员最讨厌的两件事,一件是别人不写文档,另一件就是自己写文档.这里如果把文档换成单元测试也同样成立. 每个开发人员都明白单元测试 ...

  5. 你真的会写单测吗?TDD初体验

    前言: 昨天读到了一篇文章,讲的是TDD,即Test-Driven Development,测试驱动开发.大体意思是,它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过 ...

  6. Java 单测 回滚

    Java 在单测的时候 需要做回滚 设置如下: 需要添加以下 注解 在类上 defaultRollback = true : 为 默认全部回滚 defaultRollback = false : 为 ...

  7. 使用Java函数接口及lambda表达式隔离和模拟外部依赖更容易滴单测

    概述 单测是提升软件质量的有力手段.然而,由于编程语言上的支持不力,以及一些不好的编程习惯,导致编写单测很困难. 最容易理解最容易编写的单测,莫过于独立函数的单测.所谓独立函数,就是只依赖于传入的参数 ...

  8. 输入输出无依赖型函数的GroovySpock单测模板的自动生成工具(上)

    目标 在<使用Groovy+Spock轻松写出更简洁的单测> 一文中,讲解了如何使用 Groovy + Spock 写出简洁易懂的单测. 对于相对简单的无外部服务依赖型函数,通常可以使用 ...

  9. 一分钟了解ruby中的单测

    之前用gtest写过很多c++的单测case, 对gtest的强大和灵活印象深刻:最近需要用ruby写一个小工具, 接触了下ruby, 写了代码就要写单测啊(好的单测确实对代码的健壮性和正确性保证上太 ...

随机推荐

  1. drf--频率组件

    目录 频率组件简介 自定义频率类 内置频率类及局部使用 全局使用 源码分析 SimpleRateThrottle源码分析 频率组件简介 主要是为了限制用户访问的次数,比如某一个接口(发送验证码)同一个 ...

  2. 小程序mpvue怎么点击按钮获取button里面的值

    在小程序里面是没有dom元素的,这个我们只要会小程序的应该都知道,但是在平时开发中我们偶尔会遇到需要点击某个元素获取它的值的情况,在这里给大家列举了两种情况解决方法 方式一:数据绑定 这种情况的话,对 ...

  3. Crontab常用命令总结

    一.启动服务 /sbin/service crond start 二.关闭服务 /sbin/service crond stop 三.重启服务 /sbin/service crond restart ...

  4. Plan B

    王兴曾经说过: 2019 年是过去 10 年中最差的一年,也是未来 10 年中最好的一年. 之前我希望王兴预判错了,但现在我发现这位掌控着生活消费类数据的大佬应该不是扯淡. 今年的内部和外部环境真的很 ...

  5. Centos6.5基于GPT格式磁盘分区

    1.查看分区 fdisk -l 2.设置分区类型未gpt格式. parted -s /dev/sdb mklabel gpt 3.基于ext3文件系统类型格式化. mkfs.ext3 /dev/sdb ...

  6. Tessy — 嵌入式软件单元测试/ 集成测试工具

    Tessy 源自戴姆勒- 奔驰公司的软件技术实验室,由德国Hitex 公司负责全球销售及技术支持服务,是一款专门针对嵌入式软件进行单元/ 集成测试的工具.它可以对C/C++ 代码进行单元.集成测试,可 ...

  7. 2019年杭电多校第二场 1008题Harmonious Army(HDU6598+最小割+建图)

    题目链接 传送门 题意 有\(n\)个士兵,要你给他们分配职业.有\(m\)对关系,对于某一对关系\(u,v\),如果同为勇士则总能力增加\(a\),同法师则增加\(c\),一个勇士一个法师增加\(\ ...

  8. js动画--链式运动

    前面几节我们只是讲述了一种运动,这节课我将讲述链式运动:就以一个动作接着一个动作完成. 对于这个实现,我们只需要改变一下就可以实现了,设置一个回调函数. var timer; window.onloa ...

  9. P1486 [NOI2004]郁闷的出纳员[权值线段树]

    权值线段树. 我们只用维护一个人是否存在,以及他当前排名,而不关心工资的具体值,这个可以直接算. 不难发现,如果不考虑新的员工,所有员工的工资的差值是不变的. 而加进来一个新的员工时,其工资为\(x\ ...

  10. php怎么识别真实ip

    PHP 里用来获取客户端 IP 的变量有这些: $_SERVER['HTTP_CLIENT_IP'] 这个头是有的,但是很少,不一定服务器都实现了.客户端可以伪造.(推荐学习:PHP编程从入门到精通) ...