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
其他信息
- 授权协议: MIT
- 更新时间: 2026-01-09