不少小夥伴所在的公司是基於Dubbo來構建技術棧的,平常開發中必不可少要寫dubbo單測(單元測試),若是單測數據依賴已有的外部dubbo服務,通常是mock數據,若是數據比較複雜,其實mock數據也是一個不小的工做量。那有沒有更好的單測方式來代替咱們完成」mock「數據功能呢,這時能夠藉助dubbo telnet功能,獲取真實數據用在單測中使用。java
本文會先討論如何使用基於dubbo telnet的代理工具類(DubboTelnetProxy),而後再討論下mockito+DubboTelnetProxy如何進行多層次的單測,最後分析下如何讓單測變得更加智能(好比自動注入等)。(ps:關於dubbo和mockito這裏就不展開討論了,具體能夠參考對應資料~)程序員
dubbo單測其實和非dubbo單測的流程是同樣的,初始化待測試類和單測上下文,打樁而後調用,最後檢查返回結果。好比咱們經常使用mockito來跑單測,其簡單的示例以下:bootstrap
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的單元測試代碼。api
在dubbo服務機器上,咱們可使用telnet鏈接dubbo服務,而後執行invoke命令來手動調用dubbo接口並獲取結果,DubboTelnetProxy就是將這一系列的手動操做按照dubbo telnet格式固化到代碼中。在具體討論DubboTelnetProxy以前,先看下其有哪些功能,DubboTelnetProxy特色:app
話很少說,先看下DubboTelnetProxy
代碼實現:ide
@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方式避免每次都新建鏈接。工具
手動/自動指定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);
}
平常開發中,可使用mockito進行單測,保證代碼質量。在mockito中,若是想讓某個DubboTelnetProxy代理類注入到待測試中,可以使用FieldUtils工具類進行屬性注入。測試
使用DubboTelnetProxy + mockito示例以下:ui
@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);
}
要實現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)));
}
上述示例中的自動注入是程序會遞歸注入待測試類中的全部屬性,但仍是須要在代碼中先調用要"自動注入"的代碼,爲了更易用,可使用註解方式來自動注入被註解修飾的全部類或者屬性,相似於在Spring中對類屬性配置了@Resource
以後,Spring在容器啓動過程當中會自動對該屬性注入對應示例,開發者無需關注。
關於如何實現mockito+DubboTelnetProxy的註解方式自動注入,筆者就不在贅述,感興趣的小夥伴能夠參考3.1中的實現思路自行實現。
說道註解,其實想實現針對某些註解執行一些特定邏輯(好比執行自動注入),能夠在兩種階段對其處理,以下所示:
AbstractProcessor
來實現特定業務邏輯,其主要的處理邏輯就是掃描、評估和處理註解的代碼,以及生產 Java 文件。好比lombok中的@Setter
註解就是要產生對應屬性的setter方法;以上兩種自動注入方式在實現都是OK的,前者在編譯階段後者在運行時,不事後者因爲在運行時起做用,所以靈活性更大。
推薦閱讀
歡迎小夥伴關注【TopCoder】閱讀更多精彩好文。