定制 qoliber/servermock 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

qoliber/servermock

最新稳定版本:v1.0.0

Composer 安装命令:

composer require --dev qoliber/servermock

包简介

Embedded HTTP mock server for PHP integration tests

README 文档

README

An embedded HTTP mock server for PHP integration tests. Inspired by Rust's wiremock crate and similar to WireMock, but runs entirely in PHP with no Docker or JVM required.

Installation

composer require --dev qoliber/servermock

Quick Start

use Qoliber\ServerMock\Mock;
use Qoliber\ServerMock\MockServer;

// Start a mock server on a random port
$server = MockServer::start();

// Define a stub
Mock::get('/api/users/1')
    ->respondJson(['id' => 1, 'name' => 'John Doe'])
    ->mount($server);

// Make requests against it
$response = file_get_contents($server->url('/api/users/1'));
// Returns: {"id":1,"name":"John Doe"}

// Clean up
$server->shutdown();

Bulk Configuration

Configure multiple stubs at once - great for complex test scenarios:

$server->configure([
    Mock::get('/api/users')->respondJson(['users' => []]),
    Mock::get('/api/products')->respondJson(['products' => ['SKU001']]),
    Mock::post('/api/orders')->respondJson(['orderId' => '12345'], 201),
]);

Scenarios

Create reusable test scenarios:

use Qoliber\ServerMock\Scenario;

$checkoutScenario = Scenario::create('checkout')
    ->stub(Mock::get('/api/cart')->respondJson(['items' => [], 'total' => 0]))
    ->stub(Mock::post('/api/cart/add')->respondJson(['success' => true]))
    ->stub(Mock::post('/api/checkout')->respondJson(['orderId' => 'ORD-001']));

$server->applyScenario($checkoutScenario);

Or use the fluent API:

$scenario = Scenario::create('user-flow');
$scenario->get('/api/profile')->respondJson(['name' => 'John']);
$scenario->post('/api/update')->respondJson(['updated' => true]);
$scenario->applyTo($server);

Load Configuration from Files

Auto-detect format by extension:

$server->loadFromFile('fixtures/api-stubs.yaml');  // Auto-detects YAML
$server->loadFromFile('fixtures/api-stubs.json');  // Auto-detects JSON

JSON

$server->loadFromJson('fixtures/stubs.json');
{
  "stubs": [
    {
      "method": "GET",
      "path": "/api/users",
      "response": { "status": 200, "body": "{\"users\": []}" }
    }
  ]
}

YAML (requires symfony/yaml)

composer require symfony/yaml
$server->loadFromYaml('fixtures/stubs.yaml');
# fixtures/stubs.yaml
stubs:
  - method: GET
    path: /api/users
    response:
      status: 200
      body: '{"users": []}'

  - method: POST
    path: /api/orders
    headers:
      Content-Type: application/json
    response:
      status: 201
      body: '{"orderId": "123"}'

  - method: GET
    pathRegex: '#^/api/products/\d+$#'
    response:
      body: '{"id": "matched"}'

TOML (requires yosymfony/toml)

composer require yosymfony/toml
$server->loadFromToml('fixtures/stubs.toml');
# fixtures/stubs.toml
[[stubs]]
method = "GET"
path = "/api/users"

[stubs.response]
status = 200
body = '{"users": []}'

[[stubs]]
method = "POST"
path = "/api/orders"

[stubs.response]
status = 201
body = '{"orderId": "123"}'

PHP (native, supports dynamic values)

$server->loadFromPhp('fixtures/stubs.php');
<?php
return [
    'stubs' => [
        [
            'method' => 'GET',
            'path' => '/api/config',
            'response' => [
                'body' => json_encode(['timestamp' => time()]),
            ],
        ],
    ],
];

Array (programmatic)

$server->loadFromArray([
    'stubs' => [
        [
            'method' => 'GET',
            'path' => '/api/users',
            'response' => ['body' => '[]'],
        ],
        [
            'method' => 'GET',
            'pathRegex' => '#^/api/users/\d+$#',
            'response' => ['body' => '{"id": "matched"}'],
        ],
    ],
]);

Usage with PHPUnit

Using the Base Test Case

use Qoliber\ServerMock\ServerMockTestCase;
use Qoliber\ServerMock\Mock;

class MyIntegrationTest extends ServerMockTestCase
{
    public function testPaymentGateway(): void
    {
        Mock::post('/v1/charges')
            ->respondJson(['id' => 'ch_123', 'status' => 'succeeded'], 201)
            ->mount($this->mockServer);

        $paymentService = new PaymentService($this->getMockServerUri());
        $result = $paymentService->charge(1000, 'usd');

        $this->assertTrue($result->isSuccessful());
    }
}

Using the Trait

use PHPUnit\Framework\TestCase;
use Qoliber\ServerMock\ServerMockTrait;
use Qoliber\ServerMock\Mock;

class MyTest extends TestCase
{
    use ServerMockTrait;

    protected function setUp(): void
    {
        parent::setUp();
        $this->setUpServerMock();
    }

    protected function tearDown(): void
    {
        $this->tearDownServerMock();
        parent::tearDown();
    }
}

Templated Responses

Dynamic responses based on request data:

Mock::post('/api/echo')
    ->respondJsonTemplate('{"path": "{{request.path}}", "method": "{{request.method}}"}')
    ->mount($server);

Mock::post('/api/users')
    ->respondJsonTemplate('{"id": "{{uuid}}", "name": "{{request.jsonBody.name}}"}')
    ->mount($server);

Available template variables:

  • {{request.path}} - Request path
  • {{request.method}} - HTTP method
  • {{request.query.paramName}} - Query parameter
  • {{request.header.HeaderName}} - Header value
  • {{request.jsonBody.field}} - JSON body field (supports nesting: jsonBody.user.name)
  • {{uuid}} - Random UUID
  • {{now}} - Unix timestamp
  • {{nowIso}} - ISO 8601 timestamp
  • {{randomInt}} - Random integer

GraphQL Support

Mock::graphql()
    ->withOperationName('GetUser')
    ->respondGraphQL(['user' => ['id' => '1', 'name' => 'John']])
    ->mount($server);

Mock::graphql()
    ->withMutation()
    ->withQueryContaining('createUser')
    ->respondGraphQL(['createUser' => ['id' => '123']])
    ->mount($server);

Mock::graphql()
    ->withVariable('userId', '999')
    ->respondGraphQL(['user' => ['id' => '999']])
    ->mount($server);

// Error responses
Mock::graphql()
    ->withOperationName('FailingOp')
    ->respondGraphQLError('Something went wrong', 'INTERNAL_ERROR')
    ->mount($server);

Request Matchers

Path Matching

Mock::get('/exact/path')
Mock::getMatching('#^/users/\d+$#')  // Regex

use Qoliber\ServerMock\Matcher\PathMatcher;
Mock::given(new MethodMatcher('GET'))
    ->and(PathMatcher::startsWith('/api/'))

Header Matching

Mock::get('/secured')
    ->withHeader('Authorization')  // Header exists
    ->withHeader('X-API-Key', 'secret')  // Header equals value

Query Parameter Matching

Mock::get('/search')
    ->withQueryParam('q')  // Param exists
    ->withQueryParam('page', '1')  // Param equals value

Body Matching

Mock::post('/echo')->withBody('exact content')
Mock::post('/search')->withBodyContaining('important')
Mock::post('/validate')->withBodyMatching('/^order-\d{4}$/')
Mock::post('/api/order')->withJsonBody(['type' => 'premium'])
Mock::post('/webhook')->withJsonPath('event.type', 'payment.success')

Responses

use Qoliber\ServerMock\Response;

Mock::get('/ok')->respondOk('Body text')
Mock::get('/not-found')->respondNotFound()
Mock::get('/error')->respondServerError()
Mock::get('/json')->respondJson(['key' => 'value'])
Mock::post('/create')->respondJson(['id' => 1], 201)

Mock::get('/custom')
    ->respondWith(
        Response::create(201)
            ->withHeader('X-Custom', 'value')
            ->withBody('{"custom": true}')
    )

Response Sequences

use Qoliber\ServerMock\ResponseSequence;

$sequence = ResponseSequence::create()
    ->thenJson(['status' => 'pending'])
    ->thenJson(['status' => 'processing'])
    ->thenJson(['status' => 'complete']);

Mock::get('/job/status')
    ->respondWithSequence($sequence)
    ->mount($server);

// With repeat
$sequence = ResponseSequence::create()
    ->thenJson(['cycle' => 1])
    ->thenJson(['cycle' => 2])
    ->repeat();  // Loops: 1, 2, 1, 2...

Delay & Priority

Mock::get('/slow-endpoint')
    ->respondOk('Finally!')
    ->withDelay(2000)  // 2 second delay
    ->mount($server);

Mock::get('/api/resource')
    ->respondJson(['source' => 'specific'])
    ->withPriority(10)  // Higher priority wins
    ->mount($server);

Verification

$stub = Mock::get('/tracked')->respondOk()->mount($server);

$client->get($server->url('/tracked'));
$client->get($server->url('/tracked'));

$requests = $server->getRecordedRequests();
$count = $server->getRequestCount();  // 2

$server->assertCalled($stub);     // At least once
$server->assertNotCalled($stub);  // Never called
$server->verify($stub, 2);        // Exactly 2 times

Stateful Mocking (State Machine)

Create scenarios where responses change based on previous requests:

use Qoliber\ServerMock\State\StateMachine;

$scenario = StateMachine::create('order-flow')
    ->initialState('empty')
    ->when('empty')
        ->on(Mock::post('/cart/add'))
        ->respondJson(['success' => true, 'itemCount' => 1])
        ->transitionTo('has-items')
    ->when('has-items')
        ->on(Mock::get('/cart'))
        ->respondJson(['items' => ['SKU-001']])
        ->stay()  // Stay in current state
    ->when('has-items')
        ->on(Mock::post('/checkout'))
        ->respondJson(['orderId' => 'ORD-123'])
        ->transitionTo('ordered')
    ->when('ordered')
        ->on(Mock::get('/order/ORD-123'))
        ->respondJson(['status' => 'confirmed'])
    ->getMachine();

$server->addStateMachine($scenario);

// Now requests trigger state transitions
$client->post('/cart/add');  // empty -> has-items
$client->get('/cart');       // stays in has-items
$client->post('/checkout');  // has-items -> ordered

// Inspect and control state
$state = $server->getScenarioState('order-flow');  // 'ordered'
$server->setScenarioState('order-flow', 'empty');  // Reset manually
$server->resetScenario('order-flow');              // Reset to initial
$server->resetAllScenarios();                      // Reset all

Fault Injection

Simulate network failures and errors for resilience testing:

use Qoliber\ServerMock\Fault\Fault;

// Random server errors (500) with 30% probability
Mock::get('/api/flaky')
    ->withFault(Fault::randomServerError(0.3, 'Service temporarily unavailable'))
    ->respondJson(['data' => 'ok'])
    ->mount($server);

// Fixed delay (simulates slow network)
Mock::get('/api/slow')
    ->withFault(Fault::fixedDelay(500))  // 500ms delay
    ->respondJson(['data' => 'slow'])
    ->mount($server);

// Random delay between min and max
Mock::get('/api/variable')
    ->withFault(Fault::randomDelay(100, 1000))  // 100-1000ms
    ->respondJson(['data' => 'variable'])
    ->mount($server);

// Connection reset (simulates dropped connection)
Mock::get('/api/unstable')
    ->withFault(Fault::connectionReset(0.2))  // 20% chance
    ->respondJson(['data' => 'ok'])
    ->mount($server);

// Empty response (simulates truncated response)
Mock::get('/api/broken')
    ->withFault(Fault::emptyResponse(0.1))
    ->respondJson(['data' => 'ok'])
    ->mount($server);

// Malformed JSON (simulates corrupt response)
Mock::get('/api/corrupt')
    ->withFault(Fault::malformedJson(0.1))
    ->respondJson(['data' => 'ok'])
    ->mount($server);

// Timeout (simulates request timeout)
Mock::get('/api/timeout')
    ->withFault(Fault::timeout(0.1, 30000))  // 10% chance, 30s delay
    ->respondJson(['data' => 'ok'])
    ->mount($server);

// Chain multiple faults
Mock::get('/api/chaos')
    ->withFault(Fault::chain(
        Fault::randomDelay(50, 200),
        Fault::randomServerError(0.1)
    ))
    ->respondJson(['data' => 'ok'])
    ->mount($server);

CLI Tool

Run ServerMock as a standalone server:

# Start server on default port (8080)
./vendor/bin/servermock start

# Custom host and port
./vendor/bin/servermock start --host 0.0.0.0 --port 9090

# Load stubs from config file
./vendor/bin/servermock start --config fixtures/stubs.json

# Show help
./vendor/bin/servermock help

Proxy Mode

Forward unmatched requests to a real server:

$server->enableProxy('https://api.example.com', record: true);

// Requests that don't match any stub are forwarded
// With record: true, responses are recorded for later inspection
$recordings = $server->getProxyRecordings();

$server->disableProxy();

Why ServerMock?

Feature Guzzle MockHandler Docker + WireMock ServerMock
Real HTTP No Yes Yes
No Docker Yes No Yes
No JVM Yes No Yes
Fast startup Yes No Yes
Works with any HTTP client No Yes Yes
Bulk configuration No Yes Yes
File-based config No Yes Yes
Stateful mocking No Yes Yes
Fault injection No Yes Yes
CLI standalone No Yes Yes

Requirements

  • PHP 8.1+

License

MIT

Credits

Developed by Qoliber

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 0
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-01-09