如何使用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 应用程序。