用express创建一个REST stub server

在做项目时(尤其是在microservice或者SOA架构下),经常会发生这些情况:

  • 待测试的API已经做好了,但是整个API依赖的其他API还是未完成状态或者因为各种原因还调不通。例如某项目需要新做一个server来调银行的REST API获取存款信息,这个server很快就做完了,结果发现银行的API才完成了不到一半;
  • 待测的API调用了几个业务逻辑特别复杂的API,做了各种I/O操作等,导致每次调用这个个API的时候不等待个十几二十秒的啥都返回不过来。这种场景下,如果执行100或者更多条用例的话,等待的时间将会是个非常恐怖的数字。

这里引出曾经在API测试中碰到的两个痛点:第一个痛点是待测API依赖的环境还没有搭建完成,没法测;第二个痛点是待测API依赖环境过于庞大和复杂,返回一个结果花费太多时间。

如何缓解

这两个痛点归纳到一起,都是依赖环境的问题。仔细想想其实需要被测试的只是这个新做的/被维护的API的内部逻辑,而和其他依赖扯上关系的复杂场景的验证,应该尽量靠到e2e测试中去。

可是如果直接去掉依赖关系的话,这个API的功能可能又测不完整。想了半天,觉得做一个stub/mock server来模拟一个被测API需要的外部依赖,给它返回一个指定的结果的话,应该能达到去除依赖且不影响待测API运行的效果。

换句话说,只要待测API能够跟调真实外部依赖API一样调这个stub server中的API,然后API返回一个指定的结果给待测API。这样一来,待测API就可以脱离真实的依赖环境做自己该做的事情了。

选择工具

说道做server,这里又必须得聊到工具上面去了,这个server本质上来说也是一个测试工具。那么有没有一种简单的方式能够实现这个server呢? 或者市面上有没有现成的/可借鉴的工具可以使用呢?

我理解的是,完全自己撸出来的工具固然可以更加完美的契合到你的项目上,然而做这么一个东西你可能需要花大量的时间来尝试,而且这对自身写代码/架构/设计的能力也是一大挑战。最后你辛辛苦苦做出来的工具有很大可能性因为拖的时间太长,或者环境依赖太多,或者本身存在暂时无法解决的BUG等原因导致工具无法使用而流产。

那么我们换个方式,在开源社区找一些现成的简单的容易实现的框架来做个简单的server貌似更加靠谱。因为成本低,就算将来不适用了,维护升级甚至舍弃掉也是很方便的事情。

身为质量保证工程师(这个名字是不是高大上),工具也好,框架也好,都只是保证质量的手段。一般情况下,我觉我们应该把更多的精力和时间放到思考/实践如何保证产品质量上去。你的价值更多的体现在你的测试思想上、你的测试用例上、对质量/风险的把控等方面。必要的时候做一个伸手党~

所以选择一个框架的时候,我首先会考虑的就是它方便,轻量,有良好的可扩展性和可读性,并且学习成本低。

而express恰好是满足这些条件的框架之一。

Express js

Express官网的说法是:

Express是一个基于 Node.js 平台,快速、开放、极简的 web 开发框架。

是的没错,这是一个web开发框架。但是我们可以根据自身需要,取其中一部分拿来用就可以了。

我们可以使用express很快速很方便的构建一套REST API server,方便待测API调用。然后再指定待测API调用server后需要返回的值。这样我们的待测API的测试脚本就可以很方便的跑起来啦。

开始

安装
npm是神一般的存在(cnpm可免翻墙)

$ npm install express --save

安装完成后,我们还是老办法,自己动手试试官网给的栗子。

随便打开一个记事本之类的文本编辑器,输入下面这段代码,保存,取个名字(例如app.js),然后使用命令node app.js就可以把这个server运行起来了。

var express = require('express');
var app = express();

app.get('/', function (req, res) {
  res.send('你嚎!');
});

var server = app.listen(3000, function () {
  var host = server.address().address;
  var port = server.address().port;

  console.log('Example app listening at http://%s:%s', host, port);
});

从例子上看,这段代码先是实例化一个express:var app = express();,并且提供了一个get的API出来:app.get('/', function (req, res) {}。当有get请求发到server的根目录'/'的时候,server就用res.send('你嚎!');给返回一个字符串"你嚎!"

而且接下来用app.listen()来指定这个服务的端口。

服务成功启动

我们可以直接使用浏览器的地址栏查看get请求的返回结果:

查看示例返回结果

基本路由

而对于其他的HTTP操作,express针对基本路由的方法也有了介绍:

// 对网站首页的访问返回 "Hello World!" 字样
app.get('/', function (req, res) {
  res.send('Hello World!');
});

// 网站首页接受 POST 请求
app.post('/', function (req, res) {
  res.send('Got a POST request');
});

// /user 节点接受 PUT 请求
app.put('/user', function (req, res) {
  res.send('Got a PUT request at /user');
});

// /user 节点接受 DELETE 请求
app.delete('/user', function (req, res) {
  res.send('Got a DELETE request at /user');
});

针对目前的项目而言,有这些基本路由的用法就已经够用了..

实例

假如项目组需要新做一个"订单操作服务"(以下别称new server),这个服务需要调用一个已经存在的"订单信息服务"(以下别称order server)中的一些API(包括GET和POST),以期待返回一个订单对象。为了减少这个外部依赖order server对新做的new server的影响,我们考虑做一个"stubOrderServer"。

GET
order server会返回一个订单对象给调用方,那么我们的stubOrderServer也先构建这个返回的对象。默认情况下返回订单编号为123456的对象。

var express = require('express');
var app = express();

var order = {
    "orderId" : "123456",
    "orderStatus" : "1",
    "price" : "10",
    "isDeleted" : "0"
}

// 中间件,应用的每个请求都会执行该中间件
app.use(function(req,res,next){
    res.header('Access-Control-Allow-Methods' , 'POST, GET');
    res.header('Access-Control-Allow-Credentials' , 'ture');
    next();
})

app.get('/order',function(req,res){
    var id = req.query.id;
    if (id) {
        order.orderId = id;
    } else
    {
        order.orderId = "123456";
    }
    res.status(200).send(order);
})

var server = app.listen(3000, function () {
  console.log('Stub server listening on port 3000!');
})

这段代码中app.use()指定了一个中间件,每一个请求都会给response的header中指定这两个授权相关的属性。next()是把控制权传给下一个处理器。

另外,server还提供了一个get的API,req.query.id是获取get请求中名为id的参数,请求的格式是localhost:3000/order?id=xxx。当你成功访问的时候就会返回你期望的订单对象,如果不传编号则返回默认的order对象,并且返回状态码200。

返回指定订单
返回默认订单

POST
new server还需要post给order server一些数据,以处理一些简单的业务逻辑处理。同理我们的stub server也需要这个功能。

由于请求地址不变,可以考虑把get接口改造成链式路由句柄(就是串在一起写,当然也可以分开写)。另外,因为需要判断post的body(示例中用了json格式的request body)的内容做一些简单的业务逻辑,所以引入了express官方推荐的插件"bodyParser"

npm install body-parser

然后:

var bodyParser = require('body-parser');
app.route('/order')
 .get(function(req,res){
    //判断是否有id参数
    var id = req.query.id;
    if (id) {
        order.orderId = id;
    } else
    {
        order.orderId = "123456";
    }

    res.status(200).send(order);
})
.post(function(req,res){
    //判断是否有id参数
    if (id) {
        order.orderId = id;
    } else
    {
        order.orderId = "123456";
    }

    if (req.body.price > 800) {
        res.sendStatus(403);
    }

    if (req.body.orderStatus === "done") {
        order.orderStatus = "3";
    }else if (req.body.orderStatus === "refund") {
        order.orderStatus = "2";
    }else if (req.body.orderStatus === "undefind") {
        order.orderStatus = "0";
    }

    res.status(200).send(order);
})

写完以后,发现有重复的部分:判断是否有id参数。把这部分放到中间件里面去:

var express = require('express');
var bodyParser = require('body-parser');
var app = express();
app.use(bodyParser.json()); // for parsing application/json

var order = {
    "orderId" : "123456",
    "orderStatus" : "1",
    "price" : "10",
    "isDeleted" : "0"
}

app.use(function(req,res,next){
    res.header('Access-Control-Allow-Methods' , 'POST, GET, DELETE');
    res.header('Access-Control-Allow-Credentials' , 'ture');
    var id = req.query.id;
    if (id) {
        order.orderId = id;
    } else
    {
        order.orderId = "123456";
    }
    next();
})

app.route('/order')
 .get(function(req,res){
    res.status(200).send(order);
})
.post(function(req,res){
    if (req.body.price > 800) {
        res.sendStatus(403);
    }

    if (req.body.orderStatus === "done") {
        order.orderStatus = "3";
    }else if (req.body.orderStatus === "refund") {
        order.orderStatus = "2";
    }else if (req.body.orderStatus === "undefind") {
        order.orderStatus = "0";
    }

    res.status(200).send(order);
})

var server = app.listen(3000, function () {
  console.log('Stub server listening on port 3000!');
})

这样,我们的server可以处理一些简单的业务逻辑了。比如post的body中orderStatus是done的时候,就把订单对象中的状态设置为3;post过来的price大于800的时候就返回403,禁止访问。

不带ID参数的post请求
带参数的请求
Forbidden(http 403)

这下我们的server基本就可以使用了,剩下就根据待测API的一些具体行为再修修补补就搞定了。

扩展用法

express还可以用作托管静态文件,换句话说你还可以用这个做一个stub的网站..如果需要的话。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,600评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,519评论 25 707
  • 孩子幼儿时期,会出现不愿意跟其他的小朋友分享玩具,也不愿意分享他的书,就算自己不想玩也绝不会让别的小朋友动,还会振...
    万花谷阅读 564评论 0 0
  • 银河小区 我 老妈 大姨 小弟坐在沙发上 拨通了查询高考成绩的电话 电话那头很死板的播音腔说着让我控制不了自己的成...
    稳重摩羯座的狗阅读 174评论 0 0
  • 在图书馆看书,有时有预先想看的书,有时就没有,在书架边走走看看,总会有书会吸引我的,这种感觉就像是相遇。 来图书馆...
    张维成都阅读 152评论 0 0