如何使用Mountebank和Node.js模拟服务
作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。
介绍
在复杂的 面向服务的架构 (SOA) 中,程序通常需要调用多个服务来运行给定的工作流。 一旦一切就绪,这很好,但如果您正在处理的代码需要仍在开发中的服务,您可能会等待其他团队完成他们的工作,然后再开始您的工作。 此外,出于测试目的,您可能需要与外部供应商服务交互,例如天气 API 或记录保存系统。 供应商通常不会为您提供所需的环境,并且通常不会轻松控制其系统上的测试数据。 在这些情况下,未完成的服务和您无法控制的服务会使代码测试令人沮丧。
所有这些问题的解决方案是创建一个 service mock。 服务模拟是模拟您将在最终产品中使用的服务的代码,但比您在生产中使用的实际服务更轻、更简单且更易于控制。 您可以设置模拟服务以返回默认响应或特定测试数据,然后运行您有兴趣测试的软件,就好像依赖服务真的存在一样。 正因为如此,拥有一种灵活的方式来模拟服务可以使您的工作流程更快、更高效。
在企业环境中,制作模拟服务有时称为 服务虚拟化 。 服务虚拟化通常与昂贵的企业工具相关联,但您不需要昂贵的工具来模拟服务。 Mountebank 是一个免费的开源服务模拟工具,可用于模拟 HTTP 服务,包括 REST 和 SOAP 服务。 您还可以使用它来模拟 SMTP 或 TCP 请求。
在本指南中,您将使用 Node.js 和 Mountebank 构建两个灵活的服务模拟应用程序。 这两个模拟服务都将侦听 HTTP 中 REST 请求的特定端口。 除了这个简单的模拟行为之外,该服务还将从 逗号分隔值 (CSV) 文件 中检索模拟数据。 在本教程之后,您将能够模拟各种服务行为,以便更轻松地开发和测试您的应用程序。
先决条件
要学习本教程,您将需要以下内容:
- 安装在您的机器上的 Node.js 版本 8.10.0 或更高版本。 本教程将使用 8.10.0 版本。 要安装 Node.js,请查看 如何在 Ubuntu 18.04 上安装 Node.js 或 如何在 macOS 上安装 Node.js 并创建本地开发环境。
- 发出 HTTP 请求的工具,例如 cURL 或 Postman。 本教程将使用 cURL,因为它默认安装在大多数机器上; 如果您的机器没有 cURL,请参阅安装文档。
第 1 步 — 启动 Node.js 应用程序
在这一步中,您将创建一个基本的 Node.js 应用程序,该应用程序将作为 Mountebank 实例的基础以及您将在后续步骤中创建的模拟服务。
注意: Mountebank 可以通过使用命令 npm install -g mountebank 全局安装,作为独立应用程序使用。 然后,您可以使用 mb 命令运行它并使用 REST 请求添加模拟。
虽然这是启动和运行 Mountebank 的最快方法,但您自己构建 Mountebank 应用程序允许您在应用程序启动时运行一组预定义的模拟,然后您可以将其存储在源代码控制中并与您的团队共享。 本教程将手动构建 Mountebank 应用程序以利用这一点。
首先,创建一个新目录来放置您的应用程序。 您可以随意命名,但在本教程中,我们将其命名为 app:
mkdir app
使用以下命令进入新创建的目录:
cd app
要启动一个新的 Node.js 应用程序,请运行 npm init 并填写提示:
npm init
这些提示中的数据将用于填写您的 package.json 文件,该文件描述了您的应用程序是什么,它依赖于哪些包,以及它使用了哪些不同的脚本。 在 Node.js 应用程序中,脚本定义了构建、运行和测试应用程序的命令。 您可以使用提示的默认值或填写您的包名称、版本号等。
完成此命令后,您将拥有一个基本的 Node.js 应用程序,包括 package.json 文件。
现在使用以下命令安装 Mountebank npm 包:
npm install -save mountebank
此命令获取 Mountebank 包并将其安装到您的应用程序中。 确保使用 -save 标志来更新您的 package.json 文件,并将 Mountebank 作为依赖项。
接下来,将启动脚本添加到运行命令 node src/index.js 的 package.json。 此脚本将您的应用程序的入口点定义为 index.js,您将在后面的步骤中创建它。
在文本编辑器中打开 package.json。 你可以使用任何你想要的文本编辑器,但本教程将使用 nano。
nano package.json
导航到 "scripts" 部分并添加行 "start": "node src/index.js"。 这将添加一个 start 命令来运行您的应用程序。
您的 package.json 文件应与此类似,具体取决于您填写初始提示的方式:
应用程序/package.json
{
"name": "diy-service-virtualization",
"version": "1.0.0",
"description": "An application to mock services.",
"main": "index.js",
"scripts": {
"start": "node src/index.js"
},
"author": "Dustin Ewers",
"license": "MIT",
"dependencies": {
"mountebank": "^2.0.0"
}
}
您现在拥有 Mountebank 应用程序的基础,您通过创建应用程序、安装 Mountebank 并添加启动脚本来构建该应用程序。 接下来,您将添加一个设置文件来存储特定于应用程序的设置。
第 2 步 — 创建设置文件
在此步骤中,您将创建一个设置文件,用于确定 Mountebank 实例和两个模拟服务将侦听的端口。
每次运行 Mountebank 实例或模拟服务时,您都需要指定该服务将在哪个网络端口上运行(例如,http://localhost:5000/)。 通过将这些放在设置文件中,应用程序的其他部分将能够在需要知道服务和 Mountebank 实例的端口号时导入这些设置。 虽然您可以将这些作为常量直接编码到您的应用程序中,但如果您将它们存储在文件中,以后更改设置会更容易。 这样,您只需在一处更改值。
首先从您的 app 目录创建一个名为 src 的目录:
mkdir src
导航到您刚刚创建的文件夹:
cd src
创建一个名为 settings.js 的文件并在文本编辑器中打开它:
nano settings.js
接下来,为主 Mountebank 实例和稍后将创建的两个模拟服务添加端口设置:
应用程序/src/settings.js
module.exports = {
port: 5000,
hello_service_port: 5001,
customer_service_port: 5002
}
此设置文件包含三个条目:port: 5000 将端口 5000 分配给 Mountebank 主实例,hello_service_port: 5001 将端口 5001 分配给您将要使用的 Hello World 测试服务在稍后的步骤中创建,customer_service_port: 5002 将端口 5002 分配给将使用 CSV 数据响应的模拟服务应用程序。 如果这里的端口被占用,请随意将它们更改为您想要的任何内容。 module.exports = 使您的其他文件可以导入这些设置。
在这一步中,您使用 settings.js 定义 Mountebank 和您的模拟服务将侦听的端口,并使这些设置可用于您的应用程序的其他部分。 在下一步中,您将使用这些设置构建一个初始化脚本以启动 Mountebank。
第 3 步 — 构建初始化脚本
在这一步中,您将创建一个启动 Mountebank 实例的文件。 此文件将是应用程序的入口点,这意味着,当您运行应用程序时,此脚本将首先运行。 在构建新的服务模拟时,您将向该文件添加更多行。
在 src 目录中,创建一个名为 index.js 的文件并在文本编辑器中打开它:
nano index.js
要启动将在您在上一步中创建的 settings.js 文件中指定的端口上运行的 Mountebank 实例,请将以下代码添加到文件中:
应用程序/src/index.js
const mb = require('mountebank');
const settings = require('./settings');
const mbServerInstance = mb.create({
port: settings.port,
pidfile: '../mb.pid',
logfile: '../mb.log',
protofile: '../protofile.json',
ipWhitelist: ['*']
});
这段代码做了三件事。 首先,它会导入您之前安装的 Mountebank npm 包 (const mb = require('mountebank');)。 然后,它会导入您在上一步中创建的设置模块 (const settings = require('./settings');)。 最后,它使用 mb.create() 创建 Mountebank 服务器的实例。
服务器将侦听设置文件中指定的端口。 pidfile、logfile 和 protofile 参数用于 Mountebank 内部用于记录其进程 ID、指定其日志保存位置以及设置文件以加载自定义的文件协议实现。 ipWhitelist 设置指定允许哪些 IP 地址与 Mountebank 服务器通信。 在这种情况下,您向任何 IP 地址开放它。
保存并退出文件。
在此文件就位后,输入以下命令来运行您的应用程序:
npm start
命令提示符将消失,您将看到以下内容:
info: [mb:5000] mountebank v2.0.0 now taking orders - point your browser to http://localhost:5000/ for help
这意味着您的应用程序已打开并准备好接受请求。
接下来,检查您的进度。 打开一个新的终端窗口并使用 curl 向 Mountebank 服务器发送以下 GET 请求:
curl http://localhost:5000/
这将返回以下 JSON 响应:
Output{
"_links": {
"imposters": {
"href": "http://localhost:5000/imposters"
},
"config": {
"href": "http://localhost:5000/config"
},
"logs": {
"href": "http://localhost:5000/logs"
}
}
}
Mountebank 返回的 JSON 描述了可用于在 Mountebank 中添加或删除对象的三个不同端点。 通过使用 curl 向这些端点发送请求,您可以与 Mountebank 实例进行交互。
完成后,切换回第一个终端窗口并使用 CTRL + C 退出应用程序。 这将退出您的 Node.js 应用程序,以便您可以继续添加。
现在您有一个成功运行 Mountebank 实例的应用程序。 在下一步中,您将创建一个 Mountebank 客户端,该客户端使用 REST 请求将模拟服务添加到您的 Mountebank 应用程序。
第 4 步 — 构建 Mountebank 客户端
Mountebank 使用 REST API 进行通信。 您可以通过向上一步中提到的不同端点发送 HTTP 请求来管理 Mountebank 实例的资源。 要添加模拟服务,您需要向 imposters 端点发送 HTTP POST 请求。 imposter 是 Mountebank 中模拟服务的名称。 冒名顶替者可以是简单的也可以是复杂的,这取决于您在模拟中想要的行为。
在这一步中,您将构建一个 Mountebank 客户端以自动向 Mountebank 服务发送 POST 请求。 您可以使用 curl 或 Postman 向冒名顶替者端点发送 POST 请求,但每次重新启动测试服务器时都必须发送相同的请求。 如果您正在运行带有多个模拟的示例 API,则编写客户端脚本来为您执行此操作会更有效。
首先安装 node-fetch 库:
npm install -save node-fetch
node-fetch 库 为您提供了 JavaScript Fetch API 的实现,您可以使用它来编写更短的 HTTP 请求。 您可以使用标准的 http 库,但使用 node-fetch 是一种更轻量级的解决方案。
现在,创建一个客户端模块来向 Mountebank 发送请求。 您只需要发布冒名顶替者,因此该模块将有一种方法。
使用 nano 创建一个名为 mountebank-helper.js 的文件:
nano mountebank-helper.js
要设置客户端,请将以下代码放入文件中:
应用程序/src/mountebank-helper.js
const fetch = require('node-fetch');
const settings = require('./settings');
function postImposter(body) {
const url = `http://127.0.0.1:${settings.port}/imposters`;
return fetch(url, {
method:'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
}
module.exports = { postImposter };
这段代码通过拉入 node-fetch 库和您的设置文件开始。 然后,此模块公开一个名为 postImposter 的函数,该函数将服务模拟发布到 Mountebank。 接下来,body: 确定该函数采用 JavaScript 对象 JSON.stringify(body)。 这个对象就是你要 POST 到 Mountebank 服务的对象。 由于此方法在本地运行,因此您针对 127.0.0.1 (localhost) 运行您的请求。 fetch 方法获取参数中发送的对象,向 url 发送 POST 请求。
在这一步中,您创建了一个 Mountebank 客户端以将新的模拟服务发布到 Mountebank 服务器。 在下一步中,您将使用此客户端创建您的第一个模拟服务。
第 5 步 - 创建您的第一个模拟服务
在前面的步骤中,您构建了一个创建 Mountebank 服务器和调用该服务器的代码的应用程序。 现在是时候使用该代码来构建冒名顶替者或模拟服务了。
在 Mountebank 中,每个冒名顶替者都包含 存根 。 存根是确定冒名顶替者将给出的响应的配置集。 存根可以进一步分为 谓词和响应 的组合。 谓词是触发冒名顶替者响应的规则。 谓词可以使用许多不同类型的信息,包括 URL、请求内容(使用 XML 或 JSON)和 HTTP 方法。
从 Model-View-Controller (MVC) 应用程序的角度来看,冒名顶替者的行为类似于控制器,而存根类似于该控制器中的操作。 谓词是指向特定控制器操作的路由规则。
要创建您的第一个模拟服务,请创建一个名为 hello-service.js 的文件。 该文件将包含模拟服务的定义。
在文本编辑器中打开 hello-service.js:
nano hello-service.js
然后添加以下代码:
应用程序/src/hello-service.js
const mbHelper = require('./mountebank-helper');
const settings = require('./settings');
function addService() {
const response = { message: "hello world" }
const stubs = [
{
predicates: [ {
equals: {
method: "GET",
"path": "/"
}
}],
responses: [
{
is: {
statusCode: 200,
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(response)
}
}
]
}
];
const imposter = {
port: settings.hello_service_port,
protocol: 'http',
stubs: stubs
};
return mbHelper.postImposter(imposter);
}
module.exports = { addService };
此代码定义了一个冒名顶替者,其中包含一个包含谓词和响应的存根。 然后它将该对象发送到 Mountebank 服务器。 此代码将添加一个新的模拟服务,该服务侦听 GET 对根 url 的请求,并在收到时返回 { message: "hello world" }。
让我们看一下前面代码创建的 addService() 函数。 首先,它定义了一个响应消息hello world:
const response = { message: "hello world" }
...
然后,它定义了一个存根:
...
const stubs = [
{
predicates: [ {
equals: {
method: "GET",
"path": "/"
}
}],
responses: [
{
is: {
statusCode: 200,
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(response)
}
}
]
}
];
...
这个存根有两个部分。 谓词部分正在寻找对根 (/) URL 的 GET 请求。 这意味着当有人向模拟服务的根 URL 发送 GET 请求时,stubs 将返回响应。 存根的第二部分是 responses 数组。 在这种情况下,有一个响应,它返回一个带有 200 HTTP 状态代码的 JSON 结果。
最后一步定义了一个包含该存根的冒名顶替者:
...
const imposter = {
port: settings.hello_service_port,
protocol: 'http',
stubs: stubs
};
...
这是您要发送到 /imposters 端点的对象,以创建一个模拟具有单个端点的服务的冒名顶替者。 上述代码通过将 port 设置为您在设置文件中确定的端口、将 protocol 设置为 HTTP 并将 stubs 指定为冒名顶替者的存根来定义您的冒名顶替者。
现在您有了一个模拟服务,代码将它发送到 Mountebank 服务器:
...
return mbHelper.postImposter(imposter);
...
如前所述,Mountebank 使用 REST API 来管理其对象。 上述代码使用您之前定义的 postImposter() 函数向服务器发送 POST 请求以激活服务。
完成 hello-service.js 后,保存并退出文件。
接下来,调用index.js中新建的addService()函数。 在文本编辑器中打开文件:
nano index.js
要确保在创建 Mountebank 实例时调用该函数,请添加以下突出显示的行:
应用程序/src/index.js
const mb = require('mountebank');
const settings = require('./settings');
const helloService = require('./hello-service');
const mbServerInstance = mb.create({
port: settings.port,
pidfile: '../mb.pid',
logfile: '../mb.log',
protofile: '../protofile.json',
ipWhitelist: ['*']
});
mbServerInstance.then(function() {
helloService.addService();
});
当一个 Mountebank 实例被创建时,它返回一个 promise。 Promise 是一个直到稍后才确定其值的对象。 这可用于简化异步函数调用。 在前面的代码中,.then(function(){...}) 函数在 Mountebank 服务器初始化时执行,这发生在 Promise 解析时。
保存并退出index.js。
要测试 Mountebank 初始化时是否创建了模拟服务,请启动应用程序:
npm start
Node.js进程会占用终端,所以打开一个新的终端窗口,向http://localhost:5001/发送一个GET请求:
curl http://localhost:5001
您将收到以下响应,表示该服务正在运行:
Output{"message": "hello world"}
现在您已经测试了您的应用程序,切换回第一个终端窗口并使用 CTRL + C 退出 Node.js 应用程序。
在这一步中,您创建了第一个模拟服务。 这是一个测试服务模拟,它返回 hello world 以响应 GET 请求。 这个模拟是为了演示目的; 它并没有真正为您提供通过构建小型 Express 应用程序无法获得的任何东西。 在下一步中,您将创建一个更复杂的模拟,以利用 Mountebank 的一些功能。
第 6 步 — 构建数据支持的模拟服务
虽然您在上一步中创建的服务类型适用于某些场景,但大多数测试需要一组更复杂的响应。 在此步骤中,您将创建一个从 URL 获取参数并使用它在 CSV 文件中查找记录的服务。
首先,回到主 app 目录:
cd ~/app
创建一个名为 data 的文件夹:
mkdir data
为您的客户数据打开一个名为 customers.csv 的文件:
nano data/customers.csv
添加以下测试数据,以便您的模拟服务可以检索:
应用程序/数据/customers.csv
id,first_name,last_name,email,favorite_color 1,Erda,Birkin,ebirkinb@google.com.hk,Aquamarine 2,Cherey,Endacott,cendacottc@freewebs.com,Fuscia 3,Shalom,Westoff,swestoffd@about.me,Red 4,Jo,Goulborne,jgoulbornee@example.com,Red
这是由 API 模拟工具 Mockaroo 生成的虚假客户数据,类似于您加载到服务本身的客户表中的虚假数据。
保存并退出文件。
然后,在 src 目录中创建一个名为 customer-service.js 的新模块:
nano src/customer-service.js
要创建一个在 /customers/ 端点上侦听 GET 请求的冒名顶替者,请添加以下代码:
应用程序/src/customer-service.js
const mbHelper = require('./mountebank-helper');
const settings = require('./settings');
function addService() {
const stubs = [
{
predicates: [{
and: [
{ equals: { method: "GET" } },
{ startsWith: { "path": "/customers/" } }
]
}],
responses: [
{
is: {
statusCode: 200,
headers: {
"Content-Type": "application/json"
},
body: '{ "firstName": "${row}[first_name]", "lastName": "${row}[last_name]", "favColor": "${row}[favorite_color]" }'
},
_behaviors: {
lookup: [
{
"key": {
"from": "path",
"using": { "method": "regex", "selector": "/customers/(.*)$" },
"index": 1
},
"fromDataSource": {
"csv": {
"path": "data/customers.csv",
"keyColumn": "id"
}
},
"into": "${row}"
}
]
}
}
]
}
];
const imposter = {
port: settings.customer_service_port,
protocol: 'http',
stubs: stubs
};
return mbHelper.postImposter(imposter);
}
module.exports = { addService };
此代码定义了一个服务模拟,用于查找 URL 格式为 customers/<id> 的 GET 请求。 收到请求后,会查询客户的 id 的 URL,然后从 CSV 文件中返回相应的记录。
与您在上一步中创建的 hello 服务相比,此代码使用了更多的 Mountebank 功能。 首先,它使用了 Mountebank 的一个特性,称为 behaviors。 行为是向存根添加功能的一种方式。 在这种情况下,您使用 lookup 行为在 CSV 文件中查找记录:
...
_behaviors: {
lookup: [
{
"key": {
"from": "path",
"using": { "method": "regex", "selector": "/customers/(.*)$" },
"index": 1
},
"fromDataSource": {
"csv": {
"path": "data/customers.csv",
"keyColumn": "id"
}
},
"into": "${row}"
}
]
}
...
key 属性使用正则表达式来解析传入路径。 在这种情况下,您将使用 URL 中 customers/ 之后的 id。
fromDataSource 属性指向您用来存储测试数据的文件。
into 属性将结果注入变量 ${row}。 该变量在以下 body 部分中引用:
...
is: {
statusCode: 200,
headers: {
"Content-Type": "application/json"
},
body: '{ "firstName": "${row}[first_name]", "lastName": "${row}[last_name]", "favColor": "${row}[favorite_color]" }'
},
...
行变量用于填充响应的正文。 在这种情况下,它是一个带有客户数据的 JSON 字符串。
保存并退出文件。
接下来,打开 index.js 将新的服务模拟添加到您的初始化函数中:
nano src/index.js
添加突出显示的行:
应用程序/src/index.js
const mb = require('mountebank');
const settings = require('./settings');
const helloService = require('./hello-service');
const customerService = require('./customer-service');
const mbServerInstance = mb.create({
port: settings.port,
pidfile: '../mb.pid',
logfile: '../mb.log',
protofile: '../protofile.json',
ipWhitelist: ['*']
});
mbServerInstance.then(function() {
helloService.addService();
customerService.addService();
});
保存并退出文件。
现在用 npm start 启动 Mountebank。 这将隐藏提示,因此打开另一个终端窗口。 通过向 localhost:5002/customers/3 发送 GET 请求来测试您的服务。 这将在 id 3 下查找客户信息。
curl localhost:5002/customers/3
您将看到以下响应:
Output{
"firstName": "Shalom",
"lastName": "Westoff",
"favColor": "Red"
}
在此步骤中,您创建了一个从 CSV 文件读取数据并将其作为 JSON 响应返回的模拟服务。 从这里开始,您可以继续构建与您需要测试的服务相匹配的更复杂的模拟。
结论
在本文中,您使用 Mountebank 和 Node.js 创建了自己的服务模拟应用程序。 现在您可以构建模拟服务并与您的团队共享它们。 无论是涉及您需要测试的供应商服务的复杂场景,还是在等待另一个团队完成工作时的简单模拟,您都可以通过创建模拟服务来让您的团队继续前进。
如果您想了解有关 Mountebank 的更多信息,请查看他们的 文档 。 如果您想将此应用程序容器化,请查看 Containerizing a Node.js Application for Development With Docker Compose 。 如果您想在类似生产的环境中运行此应用程序,请查看 如何在 Ubuntu 18.04 上为生产设置 Node.js 应用程序。