如何在PHPUnit中Mock Worldpay自表单订阅支付功能?
Great question! Since the Worldpay "own form" relies on JavaScript to generate the payment token and pass it to your backend as $_POST['token'], you don't need to mock the frontend JS flow at all. Your PHPUnit tests should focus solely on how your backend handles the token once it's received, plus mocking the Worldpay API calls to avoid real transactions. Here's a step-by-step guide with concrete examples:
1. Key Principles to Follow
First, let's clarify the separation of concerns:
- The frontend JS's job is to generate a valid token (Worldpay handles the security here)
- Your backend's job is to receive that token, validate its presence, and use it to call Worldpay's API to process the subscription
- Your tests should validate your backend logic, not Worldpay's JS or API (we'll mock those parts)
2. Example Test Setup
Assume you have a controller like SubscriptionController with a processPayment() method that handles the form submission. First, make sure your code uses dependency injection for the Worldpay service (this is critical for easy mocking).
Sample Controller Code
namespace App\Controllers; use App\Services\WorldpayService; use App\Exceptions\WorldpayPaymentException; class SubscriptionController { private $worldpayService; public function __construct(WorldpayService $worldpayService) { $this->worldpayService = $worldpayService; } public function processPayment(): array { // Validate token exists if (!isset($_POST['token']) || trim($_POST['token']) === '') { return [ 'status' => 'error', 'message' => 'Payment token is missing' ]; } try { // Use token to create subscription via Worldpay $subscription = $this->worldpayService->createSubscription( $_POST['token'], 19.99, // Monthly price 'USD', 'Premium Plan' ); return [ 'status' => 'success', 'subscription_id' => $subscription['id'], 'message' => 'Subscription created successfully' ]; } catch (WorldpayPaymentException $e) { return [ 'status' => 'error', 'message' => $e->getMessage() ]; } } }
PHPUnit Test Class
Create a test class that mocks the WorldpayService and simulates different $_POST scenarios:
use PHPUnit\Framework\TestCase; use App\Controllers\SubscriptionController; use App\Services\WorldpayService; use App\Exceptions\WorldpayPaymentException; class SubscriptionControllerTest extends TestCase { protected function setUp(): void { parent::setUp(); // Reset $_POST before each test to avoid cross-test contamination $_POST = []; } /** @test */ public function process_payment_returns_success_with_valid_token() { // 1. Simulate a valid token being posted to the backend $_POST['token'] = 'wp-valid-test-token-12345'; // 2. Mock the WorldpayService to return a successful subscription $mockWorldpay = $this->createMock(WorldpayService::class); $mockWorldpay->expects($this->once()) ->method('createSubscription') ->with('wp-valid-test-token-12345', 19.99, 'USD', 'Premium Plan') ->willReturn(['id' => 'sub-67890']); // 3. Initialize controller with mocked service $controller = new SubscriptionController($mockWorldpay); $response = $controller->processPayment(); // 4. Assert the response is correct $this->assertEquals('success', $response['status']); $this->assertEquals('sub-67890', $response['subscription_id']); } /** @test */ public function process_payment_returns_error_when_token_is_missing() { // 1. Simulate empty POST data (no token) $_POST = []; // 2. Mock WorldpayService (it should NEVER be called here) $mockWorldpay = $this->createMock(WorldpayService::class); $mockWorldpay->expects($this->never())->method('createSubscription'); // 3. Run the controller method $controller = new SubscriptionController($mockWorldpay); $response = $controller->processPayment(); // 4. Assert error response $this->assertEquals('error', $response['status']); $this->assertEquals('Payment token is missing', $response['message']); } /** @test */ public function process_payment_returns_error_when_worldpay_rejects_token() { // 1. Simulate an invalid token $_POST['token'] = 'wp-invalid-test-token-54321'; // 2. Mock WorldpayService to throw an exception $mockWorldpay = $this->createMock(WorldpayService::class); $mockWorldpay->expects($this->once()) ->method('createSubscription') ->with('wp-invalid-test-token-54321', 19.99, 'USD', 'Premium Plan') ->willThrowException(new WorldpayPaymentException('Invalid or expired payment token')); // 3. Run the controller method $controller = new SubscriptionController($mockWorldpay); $response = $controller->processPayment(); // 4. Assert error handling works $this->assertEquals('error', $response['status']); $this->assertEquals('Invalid or expired payment token', $response['message']); } }
3. Additional Tips
- Refactor if needed: If your current code uses a global Worldpay client instead of dependency injection, refactor it to accept the service via constructor injection. This makes mocking trivial.
- Test edge cases: Add tests for scenarios like:
- Token with whitespace (e.g.,
' wp-token-123 ') - Worldpay API returning other error types (e.g., insufficient funds, network timeout)
- Token with whitespace (e.g.,
- Avoid real API calls: Never use real Worldpay credentials in tests. Always mock the service layer to return predefined responses.
内容的提问来源于stack exchange,提问作者Andreas Christodoulou

