作者选择了开放互联网/言论自由基金作为Write for DOnations计划的一部分进行捐赠。
介绍
在复杂的面向服务的体系结构(SOA)中 ,程序通常需要调用多个服务来运行给定的工作流。 一旦一切就绪,这很好,但是如果你正在处理的代码需要一个仍处于开发阶段的服务,你可能会等待其他团队在开始你的工作之前完成他们的工作。 此外,出于测试目的,您可能需要与外部供应商服务进行交互,例如天气API或记录保存系统。 供应商通常不会为您提供所需数量的环境,并且通常无法轻松控制其系统上的测试数据。 在这些情况下,您控制之外的未完成的服务和服务可能会使代码测试变得令人沮丧。
所有这些问题的解决方案是创建服务模拟 。 服务模拟是模拟您将在最终产品中使用的服务的代码,但是比您在生产中使用的实际服务更轻,更简单,更容易控制。 您可以设置模拟服务以返回默认响应或特定测试数据,然后运行您感兴趣的软件进行测试,就好像依赖服务确实在那里一样。 因此,通过灵活的方式模拟服务可以使您的工作流程更快,更高效。
在企业环境中,制作模拟服务有时也称为服务虚拟化 。 服务虚拟化通常与昂贵的企业工具相关联,但您不需要昂贵的工具来模拟服务。 Mountebank是一个免费的开源服务模拟工具,可用于模拟HTTP服务,包括REST和SOAP服务。 您还可以使用它来模拟SMTP或TCP请求。
在本指南中,您将使用Node.js和Mountebank构建两个灵活的服务模拟应用程序。 两个模拟服务都将监听HTTP中的REST请求的特定端口。 除了这种简单的模拟行为外,该服务还将从逗号分隔值 (CSV)文件中检索模拟数据。 在本教程之后,您将能够模拟各种服务行为,以便您可以更轻松地开发和测试应用程序。
先决条件
要学习本教程,您需要具备以下条件:
您机器上安装的版本8.10.0或更高版本的Node.js. 本教程将使用版本8.10.0。 要安装Node.js,请查看如何在Ubuntu 18.04 上安装Node.js或如何在macOS上安装Node.js和创建本地开发环境 。
用于发出HTTP请求的工具,如cURL或Postman 。 本教程将使用cURL,因为它默认安装在大多数机器上; 如果您的机器没有cURL,请参阅安装文档 。
第1步 – 启动Node.js应用程序
在此步骤中,您将创建一个基本的Node.js应用程序,该应用程序将作为您的Mountebank实例的基础以及您将在后续步骤中创建的模拟服务。
注意:通过使用命令npm install -g mountebank
全局安装,可以将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
标志以使用Mountebank作为依赖项更新package.json
文件。
接下来,将一个启动脚本添加到运行命令node src/index.js
package.json
中。 此脚本将应用程序的入口点定义为index.js
,您将在稍后的步骤中创建该入口点。
在文本编辑器中打开package.json
。 您可以使用您想要的任何文本编辑器,但本教程将使用nano。
nano package.json
导航到"scripts"
部分并添加"start": "node src/index.js"
。 这将添加一个start
命令来运行您的应用程序。
您的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实例的端口设置以及稍后将创建的两个模拟服务:
module.exports = { port: 5000, hello_service_port: 5001, customer_service_port: 5002}
此设置文件有三个条目: port: 5000
将端口port: 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实例,请将以下代码添加到该文件中:
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
设置指定允许与Mountebank服务器通信的IP地址。 在这种情况下,您可以将其打开到任何IP地址。
保存并退出文件。
在此文件到位后,输入以下命令以运行您的应用程序:
npm start
命令提示符将消失,您将看到以下内容:
info: [mb:5000] mountebank v2.0.0 now taking orders - point your browser to http://localhost:5000/ for help
这意味着您的应用程序已打开并准备好接受请求。
接下来,检查您的进度。 打开一个新的终端窗口并使用curl
将以下GET
请求发送到Mountebank服务器:
curl http://localhost:5000/
这将返回以下JSON响应:
{ "_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
请求。 冒名顶替是Mountebank的模拟服务的名称。 根据您在模拟中所需的行为,Imposters可以是简单的也可以是复杂的。
在此步骤中,您将构建一个Mountebank客户端,以自动将POST
请求发送到Mountebank服务。 您可以使用curl
或Postman向imposters端点发送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
要设置客户端,请将以下代码放在文件中:
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
的函数,该函数将服务postImposter
发布到Mountebank。 接下来, body:
确定该函数采用JSON.stringify(body)
,一个JavaScript对象。 这个对象就是你要发布到Mountebank服务的内容。 由于此方法在本地运行,因此您针对127.0.0.1
( localhost
)运行请求。 fetch方法获取参数中发送的对象,并将POST
请求发送到url
。
在此步骤中,您创建了一个Mountebank客户端,以便向Mountebank服务器发布新的模拟服务。 在下一步中,您将使用此客户端创建您的第一个模拟服务。
第5步 – 创建您的第一个模拟服务
在前面的步骤中,您构建了一个应用程序,用于创建Mountebank服务器和代码以调用该服务器。 现在是时候使用该代码来构建冒名顶替者或模拟服务。
在Mountebank,每个冒名顶替者包含存根 。 存根是配置集,用于确定冒名顶替者将给出的响应。 存根可以进一步划分为谓词和响应的组合。 谓词是触发冒名顶替者响应的规则。 谓词可以使用许多不同类型的信息,包括URL,请求内容(使用XML或JSON)和HTTP方法。
从模型 – 视图 – 控制器(MVC)应用程序的角度来看,冒名顶替者像控制器一样,而存根像行动控制器中的动作。 谓词是指向特定控制器操作的路由规则。
要创建第一个模拟服务,请创建一个名为hello-service.js
的文件。 该文件将包含模拟服务的定义。
在文本编辑器中打开hello-service.js
:
nano 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服务器。 此代码将添加一个新的模拟服务,该服务监听对根url
GET
请求,并在获取一个时返回{ 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
数组。 在这种情况下,有一个响应,它返回HTTP状态代码为200
的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实例时调用该函数,请添加以下突出显示的行:
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
您将收到以下响应,表示该服务正在运行:
{"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
添加以下测试数据,以便您的模拟服务可以检索:
id,first_name,last_name,email,favorite_color 1,Erda,Birkin,[email protected],Aquamarine2,Cherey,Endacott,[email protected],Fuscia3,Shalom,Westoff,[email protected],Red4,Jo,Goulborne,[email protected],Red
这是由API 模拟工具Mockaroo生成的虚假客户数据,类似于您将加载到服务本身的customers表中的虚假数据。
保存并退出该文件。
然后,在src
目录中创建一个名为customer-service.js
的新模块:
nano src/customer-service.js
要创建监听/customers/
endpoint上的GET
请求的冒名顶替者,请添加以下代码:
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
请求。 收到请求后,它将在URL中查询客户的id
,然后从CSV文件中返回相应的记录。
此代码使用了比您在上一步中创建的hello
服务更多的Mountebank功能。 首先,它使用了Mountebank的一项称为行为的功能。 行为是一种向存根添加功能的方法。 在这种情况下,您使用lookup
行为在CSV文件中lookup
记录:
... _behaviors: { lookup: [ { "key": { "from": "path", "using": { "method": "regex", "selector": "/customers/(.*)$" }, "index": 1 }, "fromDataSource": { "csv": { "path": "data/customers.csv", "keyColumn": "id" } }, "into": "${row}" } ] }...
key
属性使用正则表达式来解析传入路径。 在这种情况下,您将获取customers/
URL中的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
添加突出显示的行:
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
您将看到以下响应:
{ "firstName": "Shalom", "lastName": "Westoff", "favColor": "Red"}
在此步骤中,您创建了一个模拟服务,该服务从CSV文件读取数据并将其作为JSON响应返回。 从这里开始,您可以继续构建更复杂的模拟,以匹配您需要测试的服务。
结论
在本文中,您使用Mountebank和Node.js创建了自己的服务模拟应用程序。 现在,您可以构建模拟服务并与团队共享。 无论是涉及供应商服务的复杂场景,还是需要在等待其他团队完成工作时进行测试,您都可以通过创建模拟服务来保持团队的移动。
如果您想了解更多关于Mountebank的信息,请查看他们的文档 。 如果您想将这个应用程序容器化,请查看使用Docker Compose对Containrizing Node.js应用程序进行开发 。 如果您想在类似生产环境中运行此应用程序,请查看如何在Ubuntu 18.04上设置Node.js生产应用程序 。