在做项目时 (尤其是在 microservice 或者 SOA 架构下),经常会发生这些情况:
这里引出曾经在 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 官网的说法是:
是的没错,这是一个 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 的网站..如果需要的话。