背景:话费流量充值原本有一套供客户端调用进行话费流量充值的接口,具体交互流程简图如下:

image

客户端提交充值的手机号码,服务端返回相对应的充值产品列表,客户端再次提交所选择的充值产品并提交订单,服务端返回订单支付信息并调起支付客户端,用户完成支付,服务端收到支付的回调后再次对支付服务端发起一次查询,确认订单信息是否存在,确认存后,充值服务端调用CP下订单接口进行下单和充值产品发放。 一切正常运行,但突然有一天,产品说要做一套不用支付流程的充值接口,一但调用下单接口且传参数校验通过,立即发放话费和流量。具体的需求场景是:用户通过参加系统某些业务如应用中心或生活服务,所做的活动,获得了100元话费,这时只需要用户提交他的手机号码,即可以实现这100元话费的发放。于是就跟开发同学商量了下,觉得在原有的接口上改造风险太大,会危及原有的充值业务,最后决定重新做一套独立的,供接入方业务的服务端调用的充值接口,即接入的业务的服务端接收到客户端传上来的手机号码,就调用这套接口对该号码进行话费或者流量的充值,不需要走支付流程。

接口的测试:

新接口开发完成并提测的时候,并没有业务接入,因此无法通过客户端对接口交进行测试,需要单独写相应的脚本进行测试。其实,原来的充值接口,即走支付流程的旧充值接口(暂称为旧充值接口,下同),有一个一直都在使用和维护的,基于HttpClient的java测试脚本,能脱离充值的客户端,单独对旧的充值接口进行测试,但由于新充值接口不走账户token和支付信息等校验,旧脚本不适用,所以重新写了新接口的测试脚本。 其实对于接口的测试,除了java脚本,也尝试过用python或者Jmeter等方式,但由于开发的代码是java,一些参数加密解密等算法可以直接拿过来用这个得天独厚的优势,所以毅然决然的选择了java脚本。 以下,以新充值接口的预查询接口为例,其他接口均以类似的方法构造。 预查询接口,是充值前查询所提交的手机号码与相应的产品id是否能够充值,具体详情如下:

image

image

首先是封装请求的header:
public class ReqHeader {

    private String version;
    private String channelId;
    private String timestamp;
    private String seqNo;
    private String signType;
    private String signNonce;
    private String sign;

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public String getChannelId() {
        return channelId;
    }

    public void setChannelId(String channelId) {
        this.channelId = channelId;
    }

    public String getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(String timestamp) {
        this.timestamp = timestamp;
    }

    public String getSeqNo() {
        return seqNo;
    }

    public void setSeqNo(String seqNo) {
        this.seqNo = seqNo;
    }

    public String getSignType() {
        return signType;
    }

    public void setSignType(String signType) {
        this.signType = signType;
    }

    public String getSignNonce() {
        return signNonce;
    }

    public void setSignNonce(String signNonce) {
        this.signNonce = signNonce;
    }

    public String getSign() {
        return sign;
    }

    public void setSign(String sign) {
        this.sign = sign;
    }

}

接着封装预查询接口请求的body:
public class PreOrderBody {

    private String phone;
    private String type;
    private String productIds;

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getProductIds() {
        return productIds;
    }

    public void setProductIds(String productIds) {
        this.productIds = productIds;
    }
}
Header参数塞值:
    public static ReqHeader buildReqHeader(){

        ReqHeader reqHeader = new ReqHeader();
        reqHeader.setVersion(version);
        reqHeader.setChannelId(channelId);
        reqHeader.setTimestamp("" + System.currentTimeMillis());
        reqHeader.setSeqNo(SeqNo);
        reqHeader.setSignType(signType);
        reqHeader.setSignNonce(SignNonce);
        return reqHeader;
    }
有没有发现:

header的参数少了sign的值?对于sign的值,开发的接口文档里面写的是:“sign: header及body中所有值为非空字段均需参与签名(值为null或空字符串都不需要参与签名),按变量名英文单词排序组成signBase=field1=ksks&….&fieldn=ksksk,然后MD5(signBase+”&key=”+appKey)”,即:所有非空参数组装+appkey(单独给接入方的key)组装起来,再取MD5,得到sign的值,因此,需要基于body和header的参数,单独构造sign:

public class ParserObjectUtils {

    public static Map<String, String> parseObjectField(Object object) {
        Map<String, String> fieldMap = new HashMap<String, String>();
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            try {
                String fieldName = field.getName();
                field.setAccessible(true);
                Object value = field.get(object);
                if (value instanceof String) {
                    fieldMap.put(fieldName, (String) value);
                }
            } catch (Exception e) {
            }
        }
        return fieldMap;
     }

    public static final String makeSignBaseWithoutNullValue(Map<String, String> params,  String[] withoutFields) {
        // 不参与签名
        for (String without : withoutFields) {
            params.remove(without);
        }
        // 过滤重复key
        List<String> keys = new ArrayList<String>(params.keySet());
        // 按照key ascii排序
        Collections.sort(keys);
        // key1=value1&=key2=value2组装待签名原始参数
        StringBuilder base = new StringBuilder();
        for (int i = 0; i < keys.size(); i++) {
            String key = (String) keys.get(i);
            String value = (String) params.get(key);
            // 值为空时不参考签名.
            if (value== null || value.length()==0) {
                continue;
            }
            base.append(key).append("=").append(value).append("&");
        }
        if (base.length() > 1) {
            base.deleteCharAt(base.length() - 1);
        }
        return base.toString();
    }
}
取MD5值:
public class MD5Util {

    public static String md5Encode(String inStr)  {
        System.out.println("MD5前:" + inStr);
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (Exception e) {
            System.out.println(e.toString());
            e.printStackTrace();
            return "";
        }

        byte[] byteArray = new byte[0];
        try {
            byteArray = inStr.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        byte[] md5Bytes = md5.digest(byteArray);
        StringBuffer hexValue = new StringBuffer();
        for (int i = 0; i < md5Bytes.length; i++) {
            int val = ((int) md5Bytes[i]) & 0xff;
            if (val < 16) {
                hexValue.append("0");
            }
            hexValue.append(Integer.toHexString(val));
        }
        System.out.println("md5后:" + hexValue);
        return hexValue.toString();
    }
签名sign:
    //签名Sign
    public static String getSign(ReqHeader reqHeader, Object body){

        Map<String, String> paramsMap = ParserObjectUtils.parseObjectField(reqHeader);
        paramsMap.putAll(ParserObjectUtils.parseObjectField(body));

        String signBase = ParserObjectUtils.makeSignBaseWithoutNullValue(paramsMap, new String []{"sign"});
        return MD5Util.md5Encode(signBase + "&key=" + channelMd5Key);
    }
组装预查询接口:
public class PreOrderReq {

    private ReqHeader header;
    private PreOrderBody body;

    public ReqHeader getHeader() {
        return header;
    }

    public void setHeader(ReqHeader header) {
        this.header = header;
    }

    public PreOrderBody getBody() {
        return body;
    }

    public void setBody(PreOrderBody body) {
        this.body = body;
    }
}
塞入header和body,以及body的参数和sign:
public static PreOrderReq generatePreOrderReq(){

        ReqHeader reqHeader = buildReqHeader();

        PreOrderBody preOrderBody = new PreOrderBody();
        preOrderBody.setPhone(phone);
        preOrderBody.setType(type);
        preOrderBody.setProductIds(productIds);

        reqHeader.setSign(getSign(reqHeader, preOrderBody));

        PreOrderReq preOrderReq = new PreOrderReq();
        preOrderReq.setHeader(reqHeader);
        preOrderReq.setBody(preOrderBody);

        return preOrderReq;
    }
将preOrderReq转换为JSONString,然后通过HttpClient进行请求:
public class RechargeServerApiTestCase extends JsonTest {

    @Test
    public void JUnitTest() throws Exception {

        PreOrderReq preOrderReq = JsonTest.generatePreOrderReq();
        String url_preOrder = "https://recharge.meizu.com/service/preorder";
        httpPostWithJSON(url_preOrder, JSON.toJSONString(preOrderReq));
    }
}
private static final String APPLICATION_JSON = "application/json";
    private static final String CONTENT_TYPE_TEXT_JSON = "text/json";

    public static void httpPostWithJSON(String url, String json) throws Exception {
        // 将JSON进行UTF-8编码,以便传输中文
        String encoderJson = URLEncoder.encode(json, HTTP.UTF_8);

        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpPost httppost = new HttpPost(url);
        httppost.addHeader(HTTP.CONTENT_TYPE, APPLICATION_JSON);

        StringEntity se = new StringEntity(json);
        se.setContentType(CONTENT_TYPE_TEXT_JSON);
        se.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, APPLICATION_JSON));
        httppost.setEntity(se);
         CloseableHttpResponse response = httpclient.execute(httppost);
        try {
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                System.out.println("-----------------------------------------");
                System.out.println("executing request " + httppost.getURI());
                String content = EntityUtils.toString(entity, "UTF-8");
                System.out.println("Requset content: " + json.toString());//打印请求内容,检查组装结果是否正确
                System.out.println("Response content:" + content);//打印请求返回内容
                JSONObject jsonObject = JSON.parseObject(content);
                System.out.println("code : " + jsonObject.getString("code"));//打印请求返回code
                System.out.println("value : " + jsonObject.getString("value"));//单独取出返回value值
                System.out.println("--------------------end-------------------");
            }
        } finally {
            response.close();
        }
    }

下订单和订单查询接口也是类型的方法,预查询、下单、订单查询方法如下:

public class RechargeServerApiTestCase extends JsonTest {

    @Test
    public void JUnitTest() throws Exception {

        PreOrderReq preOrderReq = JsonTest.generatePreOrderReq();
        String url_preOrder = "preorder_url";
        httpPostWithJSON(url_preOrder, JSON.toJSONString(preOrderReq));

        CreateOrderReq createOrderReq = JsonTest.buildCreateOrderReq();
        String url_CreateOrder = "create_url";
        httpPostWithJSON(url_CreateOrder, JSON.toJSONString(createOrderReq));
        Thread.sleep(100);

        QueryOrderReq queryOrderReq =JsonTest.buildQueryOrderReq();
        String url_QueryOrder = "query_url";
        httpPostWithJSON(url_QueryOrder, JSON.toJSONString(queryOrderReq));
    }

总结:

  • 1.方法解耦、遏制重复代码;
  • 2.参数和逻辑分离,以便传入不同参数值进行测试;
  • 3.待补充..