如何对PHP REST API做单元测试?含Web/MySQL服务Mock及Travis CI适配
Hey there! Great that you already have a grasp on writing PHPUnit tests—let's tackle your remaining questions with practical, actionable solutions.
Mocking Web Servers in PHPUnit
If your code makes HTTP requests (e.g., with Guzzle, cURL, or native functions), you don't need to hit a real web server for tests. Here are two reliable approaches:
1. Use Guzzle's Mock Handler (for Guzzle users)
Guzzle has a built-in mock system to simulate HTTP responses without network calls. Example:
use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; // Define mock responses (match what your code expects) $mockHandler = new MockHandler([ new Response(200, ['Content-Type' => 'application/json'], json_encode(['status' => 'ok', 'data' => 'test'])), // Add more responses for sequential requests if needed ]); // Create a client using the mock handler $handlerStack = HandlerStack::create($mockHandler); $client = new Client(['handler' => $handlerStack]); // Now any request made with $client will return your mock response $response = $client->get('https://api.example.com/data'); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('test', json_decode($response->getBody())->data);
2. Mock HTTP Client Interfaces (for custom/native requests)
If you're using custom HTTP logic or native functions like file_get_contents, wrap that logic in an interface and mock it with PHPUnit or Mockery. Example with Mockery:
use Mockery; use PHPUnit\Framework\TestCase; // First, define an interface for your HTTP client interface HttpClientInterface { public function get(string $url): string; } // Your actual implementation uses file_get_contents class NativeHttpClient implements HttpClientInterface { public function get(string $url): string { return file_get_contents($url); } } // In your test, mock the interface class MyServiceTest extends TestCase { public function testFetchExternalData() { $mockHttpClient = Mockery::mock(HttpClientInterface::class); $mockHttpClient->shouldReceive('get') ->with('https://api.example.com/data') ->once() ->andReturn(json_encode(['data' => 'mocked'])); // Inject the mock into your service $service = new MyService($mockHttpClient); $result = $service->fetchData(); $this->assertEquals('mocked', $result['data']); } protected function tearDown(): void { Mockery::close(); } }
Mocking MySQL Servers & Testing Database Code
You have two paths here: mocking the database client (for unit tests) or using a real test database (for integration tests).
1. Mock Database Clients (Unit Tests)
If you want to isolate your code from the actual database, mock PDO, MySQLi, or your database repository interfaces. Example mocking PDO with Mockery:
use Mockery; use PHPUnit\Framework\TestCase; class UserRepositoryTest extends TestCase { public function testGetUserById() { // Mock the PDOStatement first $mockStmt = Mockery::mock(\PDOStatement::class); $mockStmt->shouldReceive('execute') ->with([123]) ->once(); $mockStmt->shouldReceive('fetch') ->andReturn(['id' => 123, 'name' => 'Geri']); // Mock the PDO instance $mockPdo = Mockery::mock(\PDO::class); $mockPdo->shouldReceive('prepare') ->with('SELECT * FROM users WHERE id = ?') ->once() ->andReturn($mockStmt); // Inject mock PDO into your repository $repo = new UserRepository($mockPdo); $user = $repo->getById(123); $this->assertEquals('Geri', $user['name']); } protected function tearDown(): void { Mockery::close(); } }
2. Real Test Database (Integration Tests)
For testing actual SQL queries and database interactions, use a dedicated test database (never your production DB!). Here's how to set it up:
- Use SQLite In-Memory: Fast and zero setup for simple tests. Example with PDO:
$pdo = new PDO('sqlite::memory:'); // Run schema migrations for your test tables $pdo->exec('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))'); - Transaction Rollbacks: Wrap each test in a database transaction and roll it back after the test to keep data clean:
protected function setUp(): void { parent::setUp(); $this->pdo->beginTransaction(); } protected function tearDown(): void { $this->pdo->rollBack(); parent::tearDown(); } - PHPUnit Database Extension: Automatically manages test data (inserts/rollbacks) for you, supporting MySQL, PostgreSQL, and more.
Setting Up Travis CI for MySQL Tests
Travis CI has built-in support for MySQL—here's a sample .travis.yml configuration to get you started:
language: php # Test against multiple PHP versions (adjust as needed) php: - 7.4 - 8.0 - 8.1 - 8.2 # Configure MySQL version (8.0 is current stable) addons: mysql: 8.0 before_script: # Create your test database (Travis uses root with empty password by default) - mysql -u root -e "CREATE DATABASE IF NOT EXISTS my_test_db;" # Run your database migrations (adjust command to match your setup) - php bin/console doctrine:migrations:migrate --no-interaction # For Symfony # OR - php migrate.php # For custom migration scripts # Set environment variables for your test database - export DB_HOST=localhost - export DB_USER=root - export DB_PASSWORD="" - export DB_DATABASE=my_test_db # Run your PHPUnit tests script: - phpunit --configuration phpunit.xml.dist
Key Notes for Travis CI:
- Travis CI's MySQL instance has
rootuser with no password by default. - For faster tests, consider using SQLite in-memory instead of MySQL (no need for the
addonssection then). - Most common MySQL extensions (like
pdo_mysql) are pre-installed for supported PHP versions.
内容的提问来源于stack exchange,提问作者Geri Borbás

