WooCommerce Testing Guide: Plugins, Order Flows, and Payment Gateways
WooCommerce testing spans three layers: PHP unit tests for plugin logic, REST API tests for order and product management, and end-to-end browser tests for checkout flows. This guide covers all three with practical setup and examples.
WooCommerce Testing Overview
Testing a WooCommerce store is different from testing a standard application because:
- WordPress hooks — most WooCommerce functionality is triggered via
add_action/add_filter, making direct function calls insufficient - Database state — orders, products, and carts are persistent; tests must manage state carefully
- Payment gateways — real payments can't be made in tests; you need simulation
- Plugin interactions — third-party plugins may interfere with your custom plugin's behavior
The solution is a layered test strategy:
- Unit tests — test plugin PHP classes in isolation using WP_Mock
- Integration tests — test with the real WooCommerce codebase using PHPUnit + a test database
- API tests — test WooCommerce REST API endpoints
- End-to-end tests — test full checkout flows in a real browser
Layer 1: Unit Testing WooCommerce Plugins
Setup with WP_Mock
WP_Mock replaces WordPress functions with stubs, letting you test plugin code without a running WordPress install:
composer require --dev 10up/wp_mock mockery/mockery phpunit/phpunit<!-- phpunit.xml -->
<phpunit bootstrap="tests/bootstrap.php">
<testsuites>
<testsuite name="Unit">
<directory>tests/unit</directory>
</testsuite>
</testsuites>
</phpunit><?php
// tests/bootstrap.php
require_once __DIR__ . '/../vendor/autoload.php';
\WP_Mock\Bootstrap::init();Testing a Custom Discount Plugin
<?php
// src/class-bulk-discount.php
class Bulk_Discount {
public function apply_discount(float $cart_total, int $item_count): float {
if ($item_count >= 10) {
$discount = apply_filters('bulk_discount_rate', 0.10);
return $cart_total * (1 - $discount);
}
return $cart_total;
}
public function register_hooks(): void {
add_action('woocommerce_cart_calculate_fees', [$this, 'add_bulk_discount_fee']);
}
}<?php
// tests/unit/test-bulk-discount.php
use WP_Mock\Tools\TestCase;
class Test_Bulk_Discount extends TestCase {
public function setUp(): void {
parent::setUp();
WP_Mock::setUp();
}
public function tearDown(): void {
WP_Mock::tearDown();
parent::tearDown();
}
public function test_applies_10_percent_discount_for_10_or_more_items(): void {
// Mock the filter — return 10% discount rate
WP_Mock::onFilter('bulk_discount_rate')
->with(0.10)
->reply(0.10);
$discount = new Bulk_Discount();
$result = $discount->apply_discount(100.00, 10);
$this->assertEquals(90.00, $result);
}
public function test_no_discount_for_fewer_than_10_items(): void {
$discount = new Bulk_Discount();
$result = $discount->apply_discount(100.00, 9);
$this->assertEquals(100.00, $result);
// Filter should never be called for <10 items
WP_Mock::assertActionsCalled();
}
public function test_register_hooks_adds_action(): void {
WP_Mock::expectActionAdded(
'woocommerce_cart_calculate_fees',
[Mockery::type(Bulk_Discount::class), 'add_bulk_discount_fee']
);
$discount = new Bulk_Discount();
$discount->register_hooks();
}
}Layer 2: Integration Testing with WooCommerce Test Suite
The WooCommerce test suite provides a real WordPress + WooCommerce environment for integration tests:
# Install test environment
<span class="hljs-built_in">cd your-plugin/
bash bin/install-wp-tests.sh woocommerce_test root <span class="hljs-string">'' localhost latest<?php
// tests/integration/test-order-creation.php
class Test_Order_Creation extends WC_Unit_Test_Case {
public function test_creates_order_from_cart(): void {
// Create a test product
$product = WC_Helper_Product::create_simple_product();
$product->set_price(25.00);
$product->save();
// Add to cart
WC()->cart->add_to_cart($product->get_id(), 2);
// Process checkout
$order_id = WC_Helper_Checkout::create_order();
$order = wc_get_order($order_id);
// Assertions
$this->assertEquals('pending', $order->get_status());
$this->assertEquals(50.00, $order->get_total());
$this->assertCount(1, $order->get_items());
// Cleanup
$order->delete(true);
$product->delete(true);
}
public function test_order_status_transitions(): void {
$order_id = WC_Helper_Checkout::create_order();
$order = wc_get_order($order_id);
// Simulate payment confirmation
$order->payment_complete('test_transaction_123');
$this->assertEquals('processing', $order->get_status());
$this->assertEquals('test_transaction_123', $order->get_transaction_id());
// Ship the order
$order->update_status('completed', 'Order shipped.');
$this->assertEquals('completed', $order->get_status());
$order->delete(true);
}
}Testing Payment Gateways
Custom Payment Gateway Testing
<?php
class Test_Custom_Payment_Gateway extends WC_Unit_Test_Case {
protected WC_Payment_Gateway $gateway;
public function setUp(): void {
parent::setUp();
// Load your gateway class
require_once __DIR__ . '/../../includes/class-wc-gateway-custom.php';
$this->gateway = new WC_Gateway_Custom();
}
public function test_gateway_is_available_when_enabled(): void {
$this->gateway->enabled = 'yes';
$this->assertTrue($this->gateway->is_available());
}
public function test_gateway_unavailable_when_disabled(): void {
$this->gateway->enabled = 'no';
$this->assertFalse($this->gateway->is_available());
}
public function test_processes_refund_successfully(): void {
// Create a completed order with transaction ID
$order_id = WC_Helper_Checkout::create_order();
$order = wc_get_order($order_id);
$order->payment_complete('txn_abc123');
// Attempt refund
$result = $this->gateway->process_refund($order_id, 25.00, 'Customer request');
$this->assertTrue($result);
// Order note should mention refund
$notes = wc_get_order_notes(['order_id' => $order_id, 'type' => 'system']);
$this->assertNotEmpty($notes);
$order->delete(true);
}
public function test_validates_required_fields(): void {
// POST without required fields
$_POST = ['payment_method' => 'custom_gateway'];
$this->expectException(WC_Validation_Exception::class);
$this->gateway->validate_fields();
}
}Simulating Payment Success and Failure
<?php
class Test_Payment_Processing extends WC_Unit_Test_Case {
public function test_successful_payment_transitions_order_to_processing(): void {
$order_id = WC_Helper_Checkout::create_order();
$order = wc_get_order($order_id);
// Simulate payment API response
add_filter('custom_gateway_charge_response', function() {
return [
'success' => true,
'transaction_id' => 'txn_' . uniqid()
];
});
$gateway = new WC_Gateway_Custom();
$result = $gateway->process_payment($order_id);
$this->assertEquals('success', $result['result']);
$order = wc_get_order($order_id); // re-fetch
$this->assertEquals('processing', $order->get_status());
$order->delete(true);
}
public function test_failed_payment_keeps_order_pending(): void {
$order_id = WC_Helper_Checkout::create_order();
add_filter('custom_gateway_charge_response', function() {
return ['success' => false, 'error' => 'card_declined'];
});
$gateway = new WC_Gateway_Custom();
$result = $gateway->process_payment($order_id);
$this->assertEquals('fail', $result['result']);
$order = wc_get_order($order_id);
$this->assertEquals('failed', $order->get_status());
$order->delete(true);
}
}Layer 3: WooCommerce REST API Testing
Test the WooCommerce REST API to ensure your store's API is working correctly:
# tests/test_woocommerce_api.py
import pytest
import requests
from requests.auth import HTTPBasicAuth
BASE_URL = "https://your-store.test/wp-json/wc/v3"
AUTH = HTTPBasicAuth("ck_your_consumer_key", "cs_your_consumer_secret")
def test_create_product():
response = requests.post(
f"{BASE_URL}/products",
json={
"name": "Test Widget",
"type": "simple",
"regular_price": "19.99",
"manage_stock": True,
"stock_quantity": 100
},
auth=AUTH
)
assert response.status_code == 201
product = response.json()
assert product["name"] == "Test Widget"
assert float(product["regular_price"]) == 19.99
# Cleanup
requests.delete(f"{BASE_URL}/products/{product['id']}?force=true", auth=AUTH)
def test_create_and_complete_order():
# Create product
product_resp = requests.post(
f"{BASE_URL}/products",
json={"name": "Test Product", "type": "simple", "regular_price": "50.00"},
auth=AUTH
)
product_id = product_resp.json()["id"]
# Create order
order_resp = requests.post(
f"{BASE_URL}/orders",
json={
"payment_method": "bacs",
"payment_method_title": "Direct Bank Transfer",
"billing": {
"first_name": "Alice",
"last_name": "Test",
"email": "alice@test.com",
"country": "US"
},
"line_items": [{"product_id": product_id, "quantity": 2}]
},
auth=AUTH
)
assert order_resp.status_code == 201
order = order_resp.json()
assert order["status"] == "pending"
assert float(order["total"]) == 100.00
# Update to processing
update_resp = requests.put(
f"{BASE_URL}/orders/{order['id']}",
json={"status": "processing"},
auth=AUTH
)
assert update_resp.json()["status"] == "processing"
# Cleanup
requests.delete(f"{BASE_URL}/orders/{order['id']}?force=true", auth=AUTH)
requests.delete(f"{BASE_URL}/products/{product_id}?force=true", auth=AUTH)
def test_coupon_applies_correctly():
# Create a 20% off coupon
coupon_resp = requests.post(
f"{BASE_URL}/coupons",
json={
"code": "TEST20",
"discount_type": "percent",
"amount": "20",
"usage_limit": 1
},
auth=AUTH
)
assert coupon_resp.status_code == 201
coupon = coupon_resp.json()
# Create order with coupon
order_resp = requests.post(
f"{BASE_URL}/orders",
json={
"payment_method": "bacs",
"line_items": [{"product_id": 123, "quantity": 1}], # $100 product
"coupon_lines": [{"code": "TEST20"}]
},
auth=AUTH
)
order = order_resp.json()
assert float(order["total"]) == 80.00 # 20% off $100
# Cleanup
requests.delete(f"{BASE_URL}/coupons/{coupon['id']}?force=true", auth=AUTH)
requests.delete(f"{BASE_URL}/orders/{order['id']}?force=true", auth=AUTH)Layer 4: End-to-End Checkout Testing
# tests/e2e/test_checkout_flow.py — using Playwright
import pytest
from playwright.sync_api import Page, expect
def test_complete_checkout_flow(page: Page):
# Browse to product
page.goto("https://your-store.test/shop/")
page.click("text=Add to cart >> nth=0")
# Go to cart
page.goto("https://your-store.test/cart/")
expect(page.locator(".cart_item")).to_have_count(1)
# Proceed to checkout
page.click("text=Proceed to checkout")
# Fill billing details
page.fill("#billing_first_name", "Alice")
page.fill("#billing_last_name", "Test")
page.fill("#billing_email", "alice@test.com")
page.fill("#billing_address_1", "123 Test Street")
page.fill("#billing_city", "Portland")
page.select_option("#billing_state", "OR")
page.fill("#billing_postcode", "97201")
page.select_option("#billing_country", "US")
page.fill("#billing_phone", "5031234567")
# Select payment method (use test mode / bank transfer for E2E)
page.click("#payment_method_bacs")
# Place order
page.click("#place_order")
# Verify order confirmation
expect(page.locator(".woocommerce-order-received")).to_be_visible()
expect(page.locator(".order-number")).to_be_visible()CI Integration
name: WooCommerce Tests
on: [push, pull_request]
jobs:
php-unit-tests:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: woocommerce_test
options: --health-cmd="mysqladmin ping" --health-interval=10s
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: "8.2"
- run: composer install
- name: Install WC test suite
run: |
bash bin/install-wp-tests.sh woocommerce_test root root 127.0.0.1 latest
- run: ./vendor/bin/phpunit
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install pytest requests
- run: pytest tests/test_woocommerce_api.py -v
env:
WC_BASE_URL: ${{ secrets.WC_STAGING_URL }}
WC_CONSUMER_KEY: ${{ secrets.WC_CONSUMER_KEY }}
WC_CONSUMER_SECRET: ${{ secrets.WC_CONSUMER_SECRET }}WooCommerce Test Checklist
Before every release, verify:
- Product creation and update (simple, variable, grouped)
- Cart add/remove/update quantity
- Coupon application (percent, fixed, shipping)
- Checkout with each active payment gateway
- Order status transitions (pending → processing → completed)
- Refund processing
- Email notifications triggered
- Tax calculations
- Shipping rate calculation
- Stock deduction on purchase
- Out-of-stock behavior
Next Steps
- Test in staging — never run integration tests against production data
- Explore Magento 2 testing for MFTF-based functional testing
- Check e-commerce regression testing for release-time checklists
- Use HelpMeTest for 24/7 checkout flow monitoring — get alerted when your WooCommerce checkout breaks before customers do