接口测试 用 express 创建一个 REST stub server

大大灰灰狼 · 2016年07月13日 · 最后由 麦子 回复于 2016年07月14日 · 197 次阅读

在做项目时 (尤其是在 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 请求
不带ID参数的post请求

此处展示带参数的请求
带参数的请求

此处展示 Forbidden(http 403)
Forbidden(http 403)

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

扩展用法

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

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 7 条回复 时间 点赞

哥们玩 nodejs 的?

#1 楼 @mads 研发玩儿,我还只是用而已

express 都上了。。。。

#3 楼 @lihuazhang 还有啥好玩儿的推荐没..?

#5 楼 @lihuazhang 不错不错,我喜欢主页上的 Koa 的字体~

试试 mountebank,我们用的就是它

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册