通用技术 XML-RPC 在 SDK 测试中的应用举例

Yorn · December 27, 2015 · Last by 阁主夫人 replied at March 12, 2019 · 1896 hits
本帖已被设为精华帖!

0. RPC 与 XML-RPC

  • RPC: 远程过程调用(Remote Procedure Call)是一个计算机通信协议。它是一个客户端 - 服务器(Client/Server)的架构,客户端向服务端发起过程调用请求并传递参数,服务端执行完过程后将结果返回给客户端。客户端与服务端之间大部分采用接口描述语言(Interface Description Language,IDL),方便跨平台的远程过程调用。常见的比如:XML-RPC, JSON-RPC, REST-RPC 等。
  • XML-RPC: XML-RPC 是一个远程过程调用(RPC)的分布式计算协议,通过 XML 将调用函数封装,并使用 HTTP 协议作为传送机制。它支持的数据类型有:array, base64, boolean, datetime, double, integer, string, struct, nil。其格式详见 XML-RPC 的 wikixml-rpc.com。除了需要自己发送 HTTP 请求(如 JMeter),其它采用封装好的 client(如 Apache XML-RPC)来交互时,并不需要关心这些数据类型的 XML 定义。

1. 应用举例

本例使用了 Apache XML-RPC Server 端在 Android 平台上将 SDK 中的方法暴露出来,客户端分别用了 JMeter, Python, Java 的三种实现方式来对 SDK 进行测试举例。

1.0 实例清单

1.1 被测目标 SDK

SDK 中实现了 4 个方法:加法(add)、减法(subtract)、HTTP 请求查询 IP 地址的信息(httpGet)、获取 Android 设备的几条信息(getDeviceInfo)。它需要运行在 Android 平台,所以需要写一个 Android App 来运行 XML-RPC 服务。

注意本 SDK 采用了单例模式,在使用 XML-RPC 时需要小绕一下。

Target.java
package com.mrqyoung.rpc.target;


import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;


public class Target {

    private Target() {
        System.out.println("new Target");
    }

    private static class Holder {
        private static final Target target = new Target();
    }

    /**
     * SDK 通常会采用单例模式,返回唯一对象
     */
    public static Target getInstance() {
        return Holder.target;
    }

    /**
     * SDK 中的基本方法例子1-加法
     * @param i1
     * @param i2
     * @return
     */
    public int add(int i1, int i2) {
        return i1 + i2;
    }

    /**
     * SDK 中的基本方法例子1-减法
     * @param i1
     * @param i2
     * @return
     */
    public int subtract(int i1, int i2) {
        return i1 - i2;
    }

    /**
     * 在内部进行网络请求并返回内容的例子
     * 本例使用 ipip.net 的IP查询接口
     * @param String ipAddr: 需要传入IP地址
     * @return String
     * @throws Exception 
     */
    public String httpGet(String ipAddr) throws Exception {
        String url = "http://freeapi.ipip.net/" + ipAddr; 
        HttpGet httpGet = new HttpGet(url);
        HttpResponse httpResponse = new DefaultHttpClient().execute(httpGet);
        if (httpResponse == null) return "<Null>";
        int statusCode = httpResponse.getStatusLine().getStatusCode();
        if (statusCode != 200) return "<Http Error: " + statusCode +">";
        String result = EntityUtils.toString(httpResponse.getEntity());
        if (result.isEmpty()) return "<Empty>";
        return result;
    }

    /**
     * 读取 Android 设备的一些简单信息
     * @return
     * @throws JSONException
     */
    public String getDeviceInfo() throws Exception {
        JSONObject json = new JSONObject();
        json.put("deviceName", android.os.Build.MODEL);
        json.put("apiLevel", android.os.Build.VERSION.SDK_INT);
        json.put("androidVersion", android.os.Build.VERSION.RELEASE);
        json.put("manufactuer", android.os.Build.MANUFACTURER);
        json.put("product", android.os.Build.PRODUCT);
        return json.toString();
    }

}

1.2 服务端暴露出 SDK 的方法

服务端使用 Apache XML-RPC server 库,它内置了 Servlet,比较方便。
对于本 SDK,交互简单参数简单,服务端实现起来也很方便。对于可直接 new 来实例化的对象,使用代码 PropertyHandlerMapping.addHandler("Target", Target.class); 即可将 Target 类中的所有公开方法绑定到 Server,客户端通过 Target.methodName 并传入参数即可完成方法调用。

Android 服务端仅需要完成方法绑定,单例模式需要通过实现 RequestProcessorFactoryFactory 工厂类来完成实例化,并注意在退出时关闭 Server 即可。

如果 SDK 中的方法比较复杂,或者参数无法通过外部传递(如 Context),则需要自己来实现一个类,在类里面简化操作、传递特殊参数,然后将自己实现的类绑定到 Server ,相当于实现一个简单的中间件。

注意本例中有一个网络请求的方法,需要在 AndroidManifest.xml 中加上网络权限 <uses-permission android:name="android.permission.INTERNET"/>

MainActivity.java
package com.mrqyoung.rpc.usesdk;

import java.io.IOException;

import com.mrqyoung.rpc.target.Target;

import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.XmlRpcRequest;
import org.apache.xmlrpc.server.PropertyHandlerMapping;
import org.apache.xmlrpc.server.RequestProcessorFactoryFactory;
import org.apache.xmlrpc.server.XmlRpcServer;
import org.apache.xmlrpc.webserver.WebServer;

import android.os.Bundle;
import android.app.Activity;

public class MainActivity extends Activity {

    private static final int PORT = 8000;
    private WebServer webServer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        startRPCServer();

    }

    /**
     * 初始化XmlRpcServer,handle被测对象,开启服务
     */
    private void startRPCServer() {
        webServer = new WebServer(PORT);
        XmlRpcServer xmlRpcServer = webServer.getXmlRpcServer();
        PropertyHandlerMapping phm = new PropertyHandlerMapping();
        /**
         * 由于SDK中采用了单例模式,外部无法直接初始化,需要通过一个工厂类来完成初始化
         * 如果SDK中的对象可以直接new,则下面这行以及InstanceFactory类都不需要。
         */
        phm.setRequestProcessorFactoryFactory(new InstanceFactory());
        phm.setVoidMethodEnabled(true);
        try {
            /**
             * 为目标SDK添加hander,绑定到"sdk"这个名字上来,
             * 在client端调用时使用的就是"sdk"名称,后面接Target类里面的方法
             * 例如Target.add()方法,在外部对应为sdk.add()
             */
            phm.addHandler("sdk", Target.class);
        } catch (XmlRpcException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        xmlRpcServer.setHandlerMapping(phm);
        try {
            webServer.start();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /**
     * 用工厂类来对Target进行初始化
     * 参见 https://ws.apache.org/xmlrpc/handlerCreation.html
     */
    private class InstanceFactory implements RequestProcessorFactoryFactory {
        @Override
        public RequestProcessorFactory getRequestProcessorFactory(Class arg0)
                throws XmlRpcException {
            // TODO Auto-generated method stub
            return factory;
        }

        private final Target instance = Target.getInstance();
        private final RequestProcessorFactory factory = new MyRequestProcessorFactory();

        private class MyRequestProcessorFactory implements
                RequestProcessorFactory {
            public Object getRequestProcessor(XmlRpcRequest xmlRpcRequest)
                    throws XmlRpcException {
                return instance;
            }
        }
    }

    @Override
    protected void onDestroy() {
        // TODO Auto-generated method stub
        super.onDestroy();
        if (webServer != null) {
            webServer.shutdown();
        }
    }

}

1.3 JMeter 测试端 (客户端) 实现

JMeter 通过直接发送 HTTP POST 请求的方式来调用服务端的方法,并获得返回结果,可利用内置的正则和断言来判断结果。它不需要写代码,门槛低,编写 CASE 的速度较快较灵活,并且具有跨平台、命令行远程执行、定时任务自动完成等优点。缺点的需要了解 XML-RPC 中的请求参数的 XML 格式,但这点内容并不多,且可随用随查。

注意 JMeter 需要修改默认的结果编码来支持中文:修改 jmeter\bin\jmeter.properties 文件,sampleresult.default.encoding=ISO-8859-1 ,修改为 utf-8 ,并!重!启!

jscreen.png

test_mysdk.jmx 下载

1.4 Python 测试端 (客户端) 实现

Python 从 1999 年就内置了 xmlrpc 库(Python2 为 xmlrpclib,Python3 为 xmlrpc),包含 Server 和 Client ,不需要额外安装,十分方便。

作为客户端,它的使用也非常简单:

xmlrpc.client.ServerProxy(SERVER_URI).handler.method(params)

本例使用了 unittest 框架。

test_mysdk.py
# coding: utf-8


import sys
import unittest

if sys.version_info.major == 3:
    from xmlrpc.client import ServerProxy
else:
    from xmlrpclib import ServerProxy


RPC_SERVER_URI = 'http://192.168.1.4:8000'

class MySdkTests(unittest.TestCase):

    def setUp(self):
        self.sdk = ServerProxy(RPC_SERVER_URI).sdk

    def tearDown(self):
        pass

    def test_add(self, a=123, b=456):
        result = self.sdk.add(a, b)
        print('\nsdk.add(%d, %d) = %d' % (a, b, result))
        self.assertEqual(result, 579)

    def test_subtract(self, a=2015, b=1999):
        result = self.sdk.subtract(a, b)
        print('\nsdk.subtract(%d, %d) = %d' % (a, b, result))
        self.assertEqual(result, 16)

    def test_httpGet(self, ip='203.208.48.146'):
        result = self.sdk.httpGet(ip)
        print('\nsdk.httpGet(%s) is %s' % (ip, result))
        self.assertIsNotNone(result)

    def test_getDeviceInfo(self):
        result = self.sdk.getDeviceInfo()
        print('\nsdk.getDeviceInfo() is:\n%s' % (result))
        self.assertIsNotNone(result)



if __name__ == '__main__':
    unittest.main(verbosity=2)

1.5 Java 测试端 (客户端) 实现

Java 客户端使用 Apache XML-RPC Client 库,测试方法与 Python 的很相似。

TestMySDK.java
package com.mrqyoung.rpc.testmysdk;


import java.net.URL;

import junit.framework.Assert;
import junit.framework.TestCase;

import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
import org.junit.After;
import org.junit.Before;

public class TestMySDK extends TestCase {

    private static final String RPC_SERVER_URI = "http://192.168.1.4:8000/";
    private static XmlRpcClient rpc;

    @Before
    public void setUp() throws Exception {
        XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();
        config.setServerURL(new URL(RPC_SERVER_URI));
        rpc = new XmlRpcClient();
        rpc.setConfig(config);
    }

    @After
    public void tearDown() throws Exception {
    }

    public void testAdd() throws XmlRpcException {
        Object[] params = new Object[]{123, 456};
        int result = (Integer) rpc.execute("sdk.add", params);
        System.out.println("sdk.add(123, 456) = " + result);
        Assert.assertEquals(579, result);
    }

    public void testSubtract() throws XmlRpcException {
        Object[] params = new Object[]{2015, 1999};
        int result = (Integer) rpc.execute("sdk.subtract", params);
        System.out.println("sdk.subtract(2015, 1999) = " + result);
        Assert.assertEquals(16, result);
    }

    public void testHttpGet() throws XmlRpcException {
        Object[] params = new Object[]{"203.208.48.146"};
        String result = (String) rpc.execute("sdk.httpGet", params);
        System.out.println("sdk.httpGet(203.208.48.146) is " + result);
        Assert.assertNotNull(result);
    }

    public void testGetDeviceInfo() throws XmlRpcException {
        String result = (String) rpc.execute("sdk.getDeviceInfo", new Object[]{});
        System.out.println("sdk.getDeviceInfo() is:\n" + result);
        Assert.assertNotNull(result);
    }

}

1.6 服务端与客户端的其它实现

XML-RPC 有开源的 C/C++ 代码(XML-RPC for C and C++),在 iOS 上的服务端需要自行编译并实现一个 Servlet 容器(未作尝试)。而客户端相对来说是非常丰富的,支持的语言也比较多。

2. 结束语

RPC 并不是什么新技术,但它的用途还挺广,比如 Appium 采用的是 JSON-RPC ,在 Android Studio\lib\ 下也发现了一个 xmlrpc 的 jar 文件。

XML-RPC 具有简单、高效且易于实现,HTTP 协议,无关平台,适合持续集成。

特殊参数的传递、特殊方法的调用还需要自行实现中间件来完成。

共收到 5 条回复 时间 点赞
Yorn #1 · December 27, 2015 Author

FAQ:

  1. 为什么选择 XML-RPC 而不是 JSON-RPC A: 我首先是准备使用 JSON-RPC 的,奈何它的 Server 端需要自己实现一个 HTTP Servlet。而且 Python 自带了 xmlrpc 库。
Yorn #2 · December 27, 2015 Author

对比另外一种测试方案:直接写工程在调用 SDK,并在工程中完成各项测试,我认为使用 RPC 方案可以实现数据分离,并且易于扩展,测试结果输出也可以更加灵活。如果使用 JMeter 来执行,还可方便一些不会编码的人来完成测试。未完待续

楼主,没看到,怎么做客户端的接口测试啊

Yorn 回复

客户端运行的时候老是提示服务连不上,这个需要怎么连接服务

Yorn 回复

你提供的 demo 已经可以跑通了

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up