如何使用Mountebank和Node.js模拟服务

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。

介绍

在复杂的 面向服务的架构 (SOA) 中,程序通常需要调用多个服务来运行给定的工作流。 一旦一切就绪,这很好,但如果您正在处理的代码需要仍在开发中的服务,您可能会等待其他团队完成他们的工作,然后再开始您的工作。 此外,出于测试目的,您可能需要与外部供应商服务交互,例如天气 API 或记录保存系统。 供应商通常不会为您提供所需的环境,并且通常不会轻松控制其系统上的测试数据。 在这些情况下,未完成的服务和您无法控制的服务会使代码测试令人沮丧。

所有这些问题的解决方案是创建一个 service mock。 服务模拟是模拟您将在最终产品中使用的服务的代码,但比您在生产中使用的实际服务更轻、更简单且更易于控制。 您可以设置模拟服务以返回默认响应或特定测试数据,然后运行您有兴趣测试的软件,就好像依赖服务真的存在一样。 正因为如此,拥有一种灵活的方式来模拟服务可以使您的工作流程更快、更高效。

在企业环境中,制作模拟服务有时称为 服务虚拟化 。 服务虚拟化通常与昂贵的企业工具相关联,但您不需要昂贵的工具来模拟服务。 Mountebank 是一个免费的开源服务模拟工具,可用于模拟 HTTP 服务,包括 RESTSOAP 服务。 您还可以使用它来模拟 SMTPTCP 请求。

在本指南中,您将使用 Node.js 和 Mountebank 构建两个灵活的服务模拟应用程序。 这两个模拟服务都将侦听 HTTP 中 REST 请求的特定端口。 除了这个简单的模拟行为之外,该服务还将从 逗号分隔值 (CSV) 文件 中检索模拟数据。 在本教程之后,您将能够模拟各种服务行为,以便更轻松地开发和测试您的应用程序。

先决条件

要学习本教程,您将需要以下内容:

第 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.jspackage.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 服务器的实例。

服务器将侦听设置文件中指定的端口。 pidfilelogfileprotofile 参数用于 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 应用程序。