WooCommerce Testing Guide: Plugins, Order Flows, and Payment Gateways

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:

  1. Unit tests — test plugin PHP classes in isolation using WP_Mock
  2. Integration tests — test with the real WooCommerce codebase using PHPUnit + a test database
  3. API tests — test WooCommerce REST API endpoints
  4. 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

Read more