测试简介

上一章节,我们一步步开发了一个完整的服务程序DemoService。代码是写完了,如何验证我们写的代码是否正常工作呢?DemoService能否正确处理APP的请求,能否正确控制智能灯,能否正确接收智能灯的汇报消息,能否将汇报数据写入云端存储等,都少不了测试。测试根据阶段分为多种,比如单元测试、模块测试、集成测试等。考虑到后端服务的复杂性,AbleCloud提供了多种测试方案,下面会一一介绍。

单元测试

准备工作做好后,就可以开始我们的测试了,我们的单元测试采用org.apache.maven.surefire插件结合junit来完成。

注:测试用例的执行,也是通过mvn package来驱动并查看测试结果。在AbleCloud提供的示例pom.xml中,配置了该命令除了将开发的服务打包成jar文件外,也会执行单元测试代码——如果开发者编写了单元测试代码。

测试DemoService

具体的服务代码测试相对复杂,一方面其依赖的云端服务比较多;另一方面作为服务框架,在没有client,没有设备的情况下驱动测试,需要一些技巧。为此,AbleCloud为开发者提供了一系列便于测试用的功能,详细介绍如下。

测试demo

通过前面的介绍,UDS的大部分功能是由handleMsghandleDeviceMsg的各个handler提供的,因此测试工作也集中于对各个handler的测试。在单元测试过程中,无须通过任何client工具驱动,即可完成自动化的单元测试。

这里通过一个完整的测试代码演示如何对DemoService进行测试。测试代码中有详细的注释。

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class DemoServiceTest {
    // 记录起始开始测试的时间
    private static long startTime;
    // 我们会在多个测试case中用到以下的成员,并且只需要初始化一次,
    // 因此我们定义为static的
    private static ACConfiguration config;  // 测试过程的配置信息,在cloudservice-conf.xml中配置
    private static AC ac;                   // 测试用的AC框架,正式环境由框架初始化,开发者不用关注
    private static DemoService demoService; // 我们开发的服务
    private static ACAccount account;       // 用于保存测试的帐号信息
    private static ACUserDevice light;      // 用于保存测试的设备信息

    // 在所有的test case执行前初始化一次
    @BeforeClass
    public static void setUp() throws Exception {
        try {
            // 这里初始化config对象
            config = new ACConfiguration("./package/config/cloudservice-conf.xml");
            ac = AC.getTestAc(config);          // 通过AC的接口获取用于测试的ac框架

            demoService = new DemoService();
            demoService.setEnv(ac, config);     // 需要调用该接口,将框架ac赋予demoService
            demoService.init();                 // 初始化demoService

            // 使用开发者权限创建一个测试账号
            try {
                account = ac.accountMgrForTest(ac.newContext()).register("test@ablecloud.cn", "13100000000", "pswd");
            } catch (ACServiceException e) {
                if (e.getErrorCode() == 3502) {
                    // 帐号已注册
                    account = ac.accountMgrForTest(ac.newContext()).login("test@ablecloud.cn", "pswd");
                } else {
                    throw e;
                }
            }

            // 使用注册的测试账号绑定一个虚拟的测试设备
            light = ac.bindMgrForTest(ac.newContext()).bindDevice(config.getSubDomain(), "12345678", "light1", account.getUid());
            // 记录起始开始测试的时间
            startTime = System.currentTimeMillis();
        } catch (Exception e) {
            e.printStackTrace();
            fail("set up fail");
        }
    }


    @AfterClass
    public static void tearDown() throws Exception {
        // 执行完test后,需要解绑setUp绑定的测试设备,确保下次单测能顺利通过
        if (light != null && account != null) {
            ac.bindMgrForTest(ac.newContext()).unbindDevice(config.getSubDomain(), light.getId(), account.getUid());
        }
    }

    @Test
    public void test1ControlLight() throws Exception {
        if (account == null || light == null)
            return;

        try {
            // 创建一个用户的context
            ACContext context = ac.newContext(account.getUid());
            // 添加一个灯的桩
            ac.addDeviceStub(config.getSubDomain(), new LightStub());

            // 下面构造client发送的请求参数
            ACMsg req = new ACMsg();
            req.setContext(context);            // 将上面用户的context传入
            req.setName("controlLight");        // 设置请求消息的名字
            req.put("deviceId", light.getId()); // 设置要控制灯的逻辑id
            req.put("action", "on");            // "on"表示开灯

            // 构造响应消息,用于接收服务端返回的消息
            ACMsg resp = new ACMsg();
            // 这里直接调用服务的处理handler,驱动测试
            demoService.handleMsg(req, resp);
            // 服务发送消息给设备后,设备会将处理结果代码返回
            // 在我们实现的LightStub中,比较结果是否正确
            assertEquals("success", resp.get("result"));
            // 成功打开灯之后,模拟设备进行数据上报
            try {
                ACDeviceMsg deviceMsg = new ACDeviceMsg(LightMsg.REPORT_CODE, new byte[]{LightMsg.ON, LightMsg.FROM_APP});
                // 这里直接调用服务的设备消息处理handler,驱动测试
                ACDeviceReportInfo reportInfo = new ACDeviceReportInfo();
                reportInfo.setContext(ac.newContext());
                reportInfo.setDeviceId(light.getId());
                reportInfo.setPhysicalDeviceId(light.getPhysicalId());
                demoService.handleDeviceMsg(reportInfo, deviceMsg);
            } catch (Exception e) {
                e.printStackTrace();
                fail("test light report fail");
            }
        } catch (ACServiceException e) {
            e.printStackTrace();
            fail("test control light fail");
        }
    }

    @Test
    public void test2LightReportAndQuery() throws Exception {
        if (account == null || light == null)
            return;

        // 先测试智能灯上报消息,将上报数据写入设备属性中
        try {
            ACDeviceMsg acDeviceMsg = new ACDeviceMsg(LightMsg.REPORT_CODE, new byte[]{LightMsg.ON, LightMsg.FROM_SWITCH});
            // 这里直接调用服务的设备消息处理handler,驱动测试
            ACDeviceReportInfo reportInfo = new ACDeviceReportInfo();
            reportInfo.setContext(ac.newContext());
            reportInfo.setDeviceId(light.getId());
            reportInfo.setPhysicalDeviceId(light.getPhysicalId());
            demoService.handleDeviceMsg(reportInfo, acDeviceMsg);
        } catch (ACServiceException e) {
            e.printStackTrace();
            fail("test light report fail");
        }

        // 这里用上面写入设备属性存储的数据来驱动测试app发来的数据查询请求处理handler
        try {
            List<ACObject> historyDatas = ac.dstore(ac.newContext())
                    .scanHistory(light.getId())
                    .startTime(true, startTime) //true为代表闭区间包含的意思
                    .endTime(true, System.currentTimeMillis())
                    .execute();
            if (historyDatas != null) {
                assertEquals(2, historyDatas.size());
                for (ACObject zo : historyDatas) {
                    System.out.println(zo.toString());
                    //获取设备的逻辑ID
                    long deviceId = zo.getLong(ACDStore.DEVICE_ID);
                    //获取设备属性历史记录的时间戳
                    long timestamp = zo.getLong(ACDStore.TIMESTAMP);
                    assertEquals(deviceId, light.getId());
                    assertTrue(timestamp >= startTime && timestamp < System.currentTimeMillis());
                }
            }
        } catch (ACServiceException e) {
            e.printStackTrace();
            fail("test query history device data fail");
        }
    }
}

注意:可以看到,所有的单元测试用例均是直接调用handleMsghandleDeviceMsg驱动测试,无需编写或使用client工具。

此外,非常重要的一点,我们需要使用4.11及以上的junit,并且使用标签@FixMethodOrder(MethodSorters.NAME_ASCENDING)固定测试用例的执行顺序,因为我们的用例可能前后依赖。比如在test1ControlLight中写入数据,在后面的test case中会读取。因此,在为测试函数命名的时候,如果有前后依赖关系,需要考虑按ASCII字典序的命名规则。

测试桩

从前面的场景分析我们知道,开发的DemoService会和灯交互,但是我们在开发服务的过程,很可能智能灯也在研发之中,还没有发布硬件产品。我们后端服务开发者不需要也不应该等待硬件设备开发完毕才做相应的功能测试。为此,AbleCloud在服务开发框架中定义了设备桩ACDeviceStub的接口,开发者只需要依照此接口实现具体的设备桩即可。 示例的桩处理很简单,实际上你可以任意扩展,比如在桩中模拟灯的各种状态。示例代码如下:

public class LightStub extends ACDeviceStub {
    private static final Logger logger = LoggerFactory.getLogger(LightStub.class);

    public void handleControlMsg(String majorDomain, String subDomain,
                                 ACDeviceMsg req, ACDeviceMsg resp) throws Exception {
        int code = req.getCode();
        if (code != LightMsg.CODE) {
            logger.warn("got an incorrect opcode[" + code + "]");
            return;
        }
        resp.setCode(LightMsg.RESP_CODE);
        resp.setContent(new byte[]{1, 0, 0, 0});
    }
}

本地测试

单元测试通过后,我们还需要进行集成测试,因为单元测试的过程,我们并没有真正启动开发的服务,某些场景或代码路径不一定覆盖全,比如网络通信部分、服务框架部分等。 由于大部分逻辑在单元测试阶段均做了,因此集成测试相对简单,大致步骤如下:

在本地机器或任意开发机上启动服务

按照本机启动DemoService小结的说明,通过运行start.cmdstart.sh启动服务。

注意

1、运行start.cmdstart.sh的条件。执行启动命令的目录的子目录的结构要求如下所示:

/config
    /cloudservice-conf.xml
/lib
    /ablecloud-framework-1.5.6.jar
    /ac-java-api-1.6.3.jar
    /commons-collections-3.2.1.jar
    /commons-configuration-1.10.jar
    /commons-lang-2.6.jar
    /slf4j-api-1.7.7.jar
    /...
start.sh
start.cmd

2、服务启动成功后,会在根目录下生成log的文件夹,进入该文件夹查看service.log文件。若能看到如下日志,说明服务已经启动成功,可以进入下一个步骤了。

2015-09-08 17:37:47,047 INFO main:1 [ACServer.java:41:main] - Starting service...
2015-09-08 17:37:47,047 INFO main:1 [ACConfiguration.java:331:dumpConfig] - get config item[mode] value[test]
...
2015-09-08 17:37:47,047 INFO main:1 [Log.java:178:initialized] - Logging initialized @147ms
2015-09-08 17:37:47,047 INFO main:1 [Server.java:301:doStart] - jetty-9.1.5.v20140505
2015-09-08 17:37:47,047 INFO main:1 [AbstractConnector.java:266:doStart] - Started ServerConnector@4b27ad{HTTP/1.1}{0.0.0.0:8080}
2015-09-08 17:37:47,047 INFO main:1 [Server.java:350:doStart] - Started @206ms
2015-09-08 17:37:47,047 INFO main:1 [ACServer.java:80:main] - Start service DemoService ok.

服务启动成功后,即可使用任意客户端进行访问,访问过程中会实时在service.log中生成日志。

通过APP访问本机UDS

查看UDS所在的主机局域网ip地址,将ip地址设置于APP以下接口中即可实现访问本机UDS服务。

以下为Android端的代码,需要配置的代码如下:

//设置为访问本机UDS进行本地调试,参数为http://ip+":"+port,在初始化后调用即可。
AC.setSendToLocalUDS("http://192.168.1.1:8080");

注意:APP需要与UDS所在的主机处于同一局域网下

通过curl请求访问本机UDS

使用任意客户端发送http请求测试自己的接口正确性,例如用curl或自己开发的客户端都可以。以下详细介绍如何使用curl命令进行进一步测试。

注:1、AbleCloud提供的多种服务,其client和service之间的通信,底层采用http协议,方法为POST,因此任何能发送http请求的工具均可以用作服务测试的客户端。

2、 Linux 系统上如果没有 curl 则使用诸如 apt-get install curl(Ubuntu、Debian)或者 yum install curl(RedHat、Fedora)的方式来安装。 Windows 系统上安装 curl 的方法见这里

测试用的 curl 指令如下:

linux下使用curl命令

curl -v -X POST -H "Content-Type:application/x-zc-object" -H "X-Zc-Major-Domain:ablecloud" -H "X-Zc-Sub-Domain:test" -H "X-Zc-User-Id:1" -d '{"action":"I am test"}' 'http://localHost:8080/test'

windows下使用curl命令请求

curl -v -X POST -H "Content-Type:application/x-zc-object" -H "X-Zc-Major-Domain:ablecloud" -H "X-Zc-Sub-Domain:test" -H "X-Zc-User-Id:1" --data-ascii "{\"action\":\"I am test\"}" "http://localHost:8080/test"

简单解释一下上面的 curl 指令(更多 curl 用法请参考 curl 手册):

  • -v 表示 verbose 即显示 HTTP 通信交互详情。
  • -x POST 表示使用 HTTP POST 方法。
  • -H 表示 HTTP 请求头。
  • --data-ascii 表示本请求的 HTTP body 格式是 ASCII。 其余经常用到的格式还有 --data-binary,即按照字节流(octet stream)来发送请求;具体使用请参考 curl 手册,此处不赘述。
  • "http://localHost:8080/test" 表示给本地 8080 端口运行的UDS服务的 test 方法发送请求。 8080 是UDS Demo本地默认的端口号,见 /config 文件夹下的 cloudservice-conf.xml 文件,<service> <port> 配置。 test 方法是专供测试使用的一个方法,什么动作都不会触发,只回复一个空HTTP响应(没有任何 payload 的 HTTP 响应)表示请求被正常处理。

发送了上述 curl 指令后,开发者应该可以在控制台上看到类似下面的响应。

< HTTP/1.1 200 OK
< Content-Type: application/x-zc-object
< X-Zc-Msg-Name: X-Zc-Ack
< Content-Length: 0

其中:

  • 200 是 HTTP 返回码(表示 HTTP 请求正常返回);
  • X-Zc-Msg-Name 是 AbleCloud 服务框架自定义的 HTTP 请求头,当此值等于 X-Zc-Ack 时表示请求被正常处理(反之,如果是 X-Zc-Err 则表示出现了错误,并会附带错误码和错误详情);

至此,我们已完成了本地运行 UDS demo 和发送 curl 指令进行测试。

单步调试

集成测试过程中,你可能还需要对UDS服务进行单步调试以解决定位到更具体的问题,此处截图以Intellij idea为例。

注意:使用单步调试需要更新java sdk到1.4.0以上

  1. 点击上方的三角形按钮后,点击Edit Configurations...
    debug

  2. 出现如下小窗口后,点击左上角+号,选择Application
    debug

  3. 可以看到如下页面,Name默认为Unnamed,修改为工程名字,如DemoService,同时编辑Main Class,输入com.ablecloud.cloudservice.ACServer,最后确认Use classpath of module项为你的工程的module,点击OK按钮
    debug

  4. 下面可以开始debug你的工程了,首先在您的工程代码处设置断点
    debug

  5. 设置完断点后,点击上方小甲虫按钮,可以看到IntelliJ下方出现的Debug工具栏,并有"Connected to the target VM,address..."字样,说明该工程已经进入debug模式
    debug

  6. 进入debug模式后,您需要通过终端发送指令驱动您的工程,进入接口测试
    debug

  7. 发送控制指令成功后,可以看到Debug工具栏会自动进入到断点处,可以看到终端发送请求的参数等,如下图所示,点击Debug栏的向下执行按钮(或快捷键F8)继续往下执行
    debug

  8. Debug过程中,可以看到终端阻塞着等待程序的处理结果,如下图
    debug

  9. 切换到IntelliJ,继续点击向下执行,进入ACServletHandler.class的this.writeResp(reqMsg,resp)后,可以看到终端会自动出现程序处理后的返回结果,调试结束
    debug