mcustiel/phiremock-server

HTTP 和 REST 服务的模拟器

v1.4.0 2023-12-31 14:05 UTC

README

Phiremock 是一个 HTTP 服务模拟器和存根工具,允许软件开发者在开发过程中模拟 HTTP 请求并设置响应,以避免调用真实服务,特别适用于验收测试阶段,可以模拟和验证预期的 HTTP 请求。任何 HTTP 服务(例如:REST 服务)都可以使用 Phiremock 进行模拟和存根。

Phiremock 受 WireMock 极大的启发,但不会强迫你在 PHP 开发环境中安装 Java。Phiremock 的全部功能如下所示

  • 允许基于方法、头部、URL、正文内容和表单字段模拟 HTTP 请求。
  • 允许使用多个比较函数来匹配预期。
  • 提供 REST 接口进行设置。
  • 通过场景实现有状态和无状态的模拟。
  • 模拟网络延迟。
  • 在多个预期匹配请求的情况下设置优先级。如果没有设置优先级,则返回第一个匹配项。
  • 允许验证请求被执行的数量。
  • 允许从目录树中的 json 文件加载默认预期。
  • 将请求代理到另一个 URL。
  • 使用请求中的数据填充响应正文。
  • 通过 phiremock-codeception-extensionphiremock-codeception-module 与 codeception 集成。
  • 具有良好 API 的客户端支持所有功能: phiremock-client

Version Build Status Scrutinizer Code Quality Packagist Downloads

安装

默认通过 composer 安装

    "require-dev": {
        "mcustiel/phiremock-server": "^1.0",
        "guzzlehttp/guzzle": "^6.0"
    }

Phiremock Server 需要 guzzle 客户端 v6 才能运行。可以避免这个依赖项,选择任何 psr18 兼容的 HTTP 客户端,并覆盖 Phiremock Server 的工厂以提供它。

Phar

您还可以从 这里 下载独立的 server 作为 phar。

运行

执行 ./vendor/bin/phiremock

命令行参数

  • --ip|-i - 监听 HTTP 连接的网络接口。默认:0.0.0.0
  • --port|-p - 监听 HTTP 连接的端口。默认:8086
  • --expectations-dir|-e - 搜索预定义静态预期的目录。默认:[HOME_PATH]/.phiremock/expectations
  • --factory-class|f - 用于创建 phiremock 服务器所需对象的自定义工厂类。默认:默认内部工厂类。
  • --certificate|t - 启用 phiremock 监听安全连接(https)的证书文件。
  • --certificate-key|k - 证书密钥的路径。
  • --cert-passphrase|s - 如果证书被加密,则使用密码短语。
  • --debug|-d - 激活调试模式的标志。

注意:如果在使用 phiremock 的开发环境中使用文件中保存的静态预期,这非常有用。对于测试,动态设置预期可能更有用。

注意:当添加证书时,phiremock-server 将仅监听安全连接。

配置

您可以在 .phiremock 文件中静态指定 phiremock 服务器配置。其格式如下:

<?php return [
    'port'             => 8086,
    'ip'               => '0.0.0.0',
    'expectations-dir' => $_SERVER['HOME'] . '/.phiremock/expectations',
    'debug'            => false,
    'factory-class'    => '\\My\\Namespace\\FactoryClass',
    'certificate'      => null,
    'certificate-key'  => null,
    'certificate-passphrase' => null,
];

该文件将按以下顺序搜索,找到的第一个将被使用:

  1. PROJECT_ROOT_DIR/.phiremock(如果安装在 /vendor 下)
  2. PROJECT_ROOT_DIR/.phiremock.dist(如果安装在 /vendor 下)
  3. PHIREMOCK_ROOT_DIR/.phiremock(如果独立拉取)
  4. PHIREMOCK_ROOT_DIR/.phiremock.dist(如果独立拉取)
  5. $CWD/.phiremock
  6. $CWD/.phiremock.dist
  7. $HOME/.phiremock/config
  8. .phiremock(使用 PHP 的 include 路径)
  9. .phiremock.dist(使用 PHP 的 include 路径)

注意:命令行参数的优先级高于配置文件中的选项,因此如果提供,它们将覆盖这些选项。

覆盖工厂类

如果提供 Guzzle 客户端 v6 作为依赖项,则不需要额外的配置。如果您想使用不同的 HTTP 客户端,则需要将其作为 psr18 兼容的客户端提供给 phiremock 服务器。例如,如果您想使用 Guzzle 客户端 v7,则需要扩展 phiremock 服务器的工厂类。

<?php
namespace My\Namespace;

use Mcustiel\Phiremock\Client\Factory;
use GuzzleHttp;
use Psr\Http\Client\ClientInterface;

class FactoryWithGuzzle7 extends Factory
{
    public function createRemoteConnection(): ClientInterface
    {
        return new GuzzleHttp\Client();
    }
}

然后使用命令行选项或配置文件提供完全限定的类名给 phiremock-server。

注意:这仅在通过 composer 安装 phiremock 时才有效,因为它将使用与您的项目相同的供应商文件夹和自动加载器。此外,如果您拉取 phiremock 仓库并扩展 composer.json 文件。

它如何工作?

Phiremock 允许您创建应用程序需要与其通信的一些外部服务的存根版本。这可以用于在开发期间避免调用真实的应用程序,或为预期的请求设置响应。为此,您需要欺骗您的应用程序在开发阶段或测试阶段请求 phiremock 服务器。

设置应用程序的配置

首先,您需要为应用程序的不同环境设置配置。例如

    // config/production.json
    {
        "external_service": "https://service.example.com/v1/"
    }
    // config/development.json
    {
        "external_service": "http://localhost:8080/example_service_dev/"
    }
    // config/acceptance.json
    {
        "external_service": "http://localhost:8080/example_service_test/"
    }

配置期望

然后,使用 phiremock 的 REST 接口,可以通过指定给定请求的响应来配置期望。Phiremock 的 REST 期望资源如下所示:

{
    "version": "2",
    "scenarioName": null,
    "on": {
        "scenarioStateIs": null,
        "method": { "isSameString": "GET" },
        "url": { "matches": "~^/images/~"},
        "body": null,
        "headers" : null,
        "formData": null
    },
    "then": {
        "delayMillis": 100,
        "newScenarioState": null,
        "response": {
            "statusCode": 200,
            "body": "phiremock.base64:__BASE64_ENCODED_IMAGE__",
            "headers": { "Content-Type": "image/x-icon" }
        }
    },
    "priority": 0
}

相同的格式可用于 CLI 的 --expectations-dir 参数指定的目录树中保存的期望文件。为了使 Phiremock 服务器能够加载它们,每个文件都应该有 .json 扩展名。例如:上一个示例中的 match-all-images.json

功能

创建期望

要从代码中创建先前响应,应使用以下方法

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": { "isEqualTo" : "/example_service/some/resource" },
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "{\"id\": 1, \"description\": \"I am a resource\"}",
            "headers": {
                "Content-Type": "application/json"
            }
        }
    }
}

清除期望

在测试运行后,可以删除之前配置的所有期望,以便它们不会影响下一个测试的执行

DELETE /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host

列出所有期望

如果您出于某种原因想列出所有创建的期望,则提供了一个方便的端点

GET /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host

验证请求数量

为了知道一个请求发送给 Phiremock 服务器多少次,例如在测试中执行功能之后进行验证,还有一个辅助方法

POST /__phiremock/executions HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "request": {
        "method": "GET",
        "url": {
            "isEqualTo" : "/example_service/some/resource"
        }
    }
}

搜索已执行的请求

为了搜索 Phiremock 服务器响应的请求列表

PUT /__phiremock/executions HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "request": {
        "method": "GET",
        "url": {
            "isEqualTo" : "/example_service/some/resource"
        }
    }
}

重置请求日志

要重置请求计数器到 0,Phiremock 服务器还提供了一个端点

DELETE /__phiremock/executions HTTP/1.1
Host: your.phiremock.host

重置 Phiremock 到其初始状态

此调用将清除请求列表、场景、删除所有配置的期望,并重新加载期望目录中定义的静态期望。

POST /__phiremock/reset HTTP/1.1
Host: your.phiremock.host

酷炫功能

在响应中发送二进制体

二进制内容也可以通过在期望 json 中编码为 base64 作为响应体发送。

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": { "isEqualTo" : "/example_service/photo.jpg" },
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "phiremock.base64:HERE_THE_BASE64_ENCODED_IMAGE",
            "headers": {
                "Content-Type": "image/jpeg"
            }
        }
    }
}

优先级

Phiremock 接受多个可以匹配相同请求的期望。如果没有设置优先级,它将匹配第一个创建的期望,但如果你需要为某些请求提供高优先级,可以轻松做到。

假设您已经配置了以下两个期望

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": { "isEqualTo": "/example_service/some/resource"}
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "<resource id=\"1\" description=\"I am a resource\"/>",
            "headers": [ "Content-Type": "text/xml" ]
        }
    }
}
POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": { "isEqualTo": "/example_service/some/resource"},
        "headers": {
            "Accept": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "{\"id\": 1, \"description\": \"I am a resource\"}",
            "headers": [ "Content-Type": "application/json" ]
        }
    },
    "priority": 10    
}

在上一个示例中,两个期望都将匹配 URL 等于:/example_service/some/resource 且方法为 GET 的请求。但 Phiremock 将优先级更高地给予 Accept 标头等于 application/json 的那个。期望的默认优先级为 0,数字越高,优先级越高。

有状态行为

如果您想模拟一个响应依赖于之前请求中设置的某个状态的服务的状态,您可以使用场景来创建有状态的行为。

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "scenarioName": "saved",
    "on": {
        "scenarioStateIs": "Scenario.START",
        "method": { "isSameString": "POST" },
        "url": { "isEqualTo": "/example_service/some/resource"},
        "body": {"isEqualTo" : "{"\id": \"1\", \"name\" : \"resource\"}"},
        "headers": {
            "Accept": {"Content-Type": "application/json"}
        }
    },
    "then": {
        "newScenarioState": "RESOURCE_SAVED",
        "response": {
            "statusCode": 201,
            "body": "{"\id": \"1\", \"name\" : \"resource\"}",
            "headers": [ "Content-Type": "application/json" ]
        }
    }
}
POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "scenarioName": "saved",
    "on": {
        "scenarioStateIs": "RESOURCE_SAVED",
        "method": { "isSameString": "POST" },
        "url": { "isEqualTo": "/example_service/some/resource"},
        "body": {"isEqualTo" : "{"\id": \"1\", \"name\" : \"resource\"}"},
        "headers": {
            "Accept": {"Content-Type": "application/json"}
        }
    },
    "then": {
        "response": {
            "statusCode": 409,
            "body": "Resource with id = 1 was already created"
        }
    }
}

在这种情况下,当 Phiremock 服务器第一次收到符合期望的请求时,第一个期望将匹配并改变 saved 场景的状态。从第二次执行相同的请求开始,第二个期望将匹配。如果您想在第二次调用后回到初始状态,只需在 then 部分添加 "newScenarioState": "Scenario.START"

要重置所有场景到初始状态(Scenario.START),请使用客户端的此简单方法

DELETE /__phiremock/scenarios HTTP/1.1
Host: your.phiremock.host

在任何时刻定义场景状态

PUT /__phiremock/scenarios HTTP/1.1
Host: your.phiremock.host

{
    "scenarioName": "saved",
    "scenarioState": "Scenario.START"
}

网络延迟模拟

如果您想测试您的应用程序在超时等情况下的行为,您可以使 Phiremock 延迟响应您的请求,如下所示。

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": { "isEqualTo": "/example_service/some/resource"},
        "headers": {
            "Accept": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "delayMillis": 30000,
        "response": {
            "statusCode": 200
        }
    }
}

这将导致 Phiremock 服务器在发送响应前等待 30 秒。

代理

可能存在某些调用不需要模拟的情况。对于这种情况,Phiremock 提供了一个代理功能,该功能将接收到的请求原样传递到配置的 URI,并返回其真实响应。它可以配置如下

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "POST" },
        "url": { "isEqualTo": "/example_service/some/resource"},
        "headers": {
            "Accept": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "proxyTo": "http://your.real.service/some/path/script.php"
    }
}

在这种情况下,Phiremock 将使用配置的正文和头部将 POST 发送到 http://your.real.service/some/path/script.php 并返回其响应。

比较 JSON 对象

Phiremock 支持在 API 中比较 JSON 对象的严格相等性。比较是按对象进行的,所以缩进或空格不同无关紧要。

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "body": { "isSameJsonObject": "{\"some\": \"json\", \"here\": [1, 2, 3]}"},
        "headers": {
            "Content-Type": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "response": {
            "statusCode": 201,
            "body": "{\"id\": 1}"
        }
    }
}

根据请求数据生成响应

可能存在您想使响应依赖于您在请求中接收到的数据的情况。对于这些情况,您可以使用正则表达式匹配请求 URL 和/或正文,并使用 ${body.matchIndex}${url.matchIndex} 语法从响应正文规范中访问子模式匹配。

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": {"matches": "~^/example_service/(\w+)/?id=(\d+)~"}
        "body": { "matches": "~\{\"name\" : \"([^\"]+)\"\}~" },
        "headers": {
            "Content-Type": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "The resource is ${url.1}, the id is ${url.2} and the name is ${body.1}",
            "headers": {"X-id": "id is ${url.2}"}
        }
    }
}

也支持从多个匹配中检索数据

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": {"matches": "~/peoples-brothers-list/json~"}
        "body": { "matches": "%\"name\"\s*:\s*\"([^\"]*)",\s*\"brothers\"\s*:\s*(\d+)%" },
        "headers": {
            "Content-Type": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "${body.1} has ${body.2} brothers, ${body.1.2} has ${body.2.2} brothers, ${body.1.3} has ${body.2.3} brothers"
        }
    }
}

这也支持生成如以下示例所示的代理 URL

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": {"matches": "~^/example_service/(\w+)~"}
        "headers": {
            "Content-Type": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "proxyTo": "https://some.other.service/path/${url.1}"
    }
}

基于表单数据的条件

对于使用 application/x-www-form-urlencoded 编码并指定此内容类型的请求。Phiremock 服务器能够对表单字段的值执行条件。

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "matches": "~POST|PUT~" },
        "url": {"isEqualTo": "/login-form-handler"}
        "formData": {
            "username": {"isEqualTo": "the_username"},
            "password": {"isEqualTo": "the_password"},
        }
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "Login successful"
        }
    }
}

向后兼容性

Phiremock 服务器仍然支持 Phiremock V1 格式的期望。这应该使您从 Phiremock v1 迁移到 Phiremock v1(phiremock-server + phiremock-client)变得更加容易。

{
    "scenarioName": "potato",
    "scenarioStateIs": "Scenario.START",
    "newScenarioState": "tomato",
    "request": {
        "method": "GET",
        "url": {
            "isEqualTo": "/some/thing"
        },
        "headers": {
            "Content-Type": {
                "isEqualTo": "text/plain"
            }
        }
    },
    "response": {
        "statusCode": 200,
        "body": "Hello world!",
        "headers": {
            "Content-Type": "text/plain"
        }
    },
    "priority": 1
}

附录

条件匹配器列表

  • contains: 检查指定的 http 请求部分是否包含指定的字符串。
  • isEqualTo: 检查指定的 http 请求部分是否等于指定的值,区分大小写。
  • isSameString: 检查指定的 http 请求部分是否等于指定的值,不区分大小写。
  • matches: 检查指定的 http 请求部分是否与指定的正则表达式匹配。
  • isSameJsonObject: 检查请求中接收到的 JSON 是否与指定的 JSON 相同。

另请参阅

贡献

只需提交一个拉取请求。别忘了先运行测试和php-cs-fixer,并编写文档。

感谢

以及所有提交了拉取请求的人。