Testing Guide
This guide provides comprehensive documentation for testing the WooAI Chatbot Pro plugin across all layers: PHP unit and integration tests, JavaScript component tests, and end-to-end flows.
1. Testing Overview
Testing Philosophy
The WooAI Chatbot Pro testing strategy follows these core principles:
Test Isolation: Each test runs independently with clean state. Global mocks and transients are reset between tests to prevent cross-contamination.
Mock External Dependencies: AI providers, WooCommerce, and external APIs are mocked to ensure deterministic behavior and fast execution without network calls.
Hierarchical Coverage: Unit tests verify individual components, integration tests verify component interactions, and E2E tests verify complete user flows.
Fail Fast: Tests are configured with strict mode (
failOnRisky,failOnWarning) to catch potential issues early.
Test Types
| Type | Framework | Directory | Purpose |
|---|---|---|---|
| Unit | PHPUnit | tests/AI, tests/Search, tests/Chat |
Individual class/method testing |
| Integration | PHPUnit | tests/Integration |
Component interaction testing |
| E2E | PHPUnit | tests/E2E |
Complete flow testing |
| JavaScript | Jest | assets/src/**/__tests__ |
React component and hook testing |
Coverage Goals
The project enforces strict coverage thresholds:
// jest.config.js
coverageThreshold: {
global: {
lines: 85,
branches: 85,
functions: 85,
statements: 85,
},
},
PHP coverage targets: 80%+ line coverage for core modules.
2. PHP Testing (PHPUnit)
Setup
PHPUnit Configuration
The phpunit.xml configures three test suites with strict execution:
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="tests/bootstrap.php"
colors="true"
executionOrder="depends,defects"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/AI</directory>
<directory>tests/Search</directory>
<directory>tests/Chat</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="E2E">
<directory>tests/E2E</directory>
</testsuite>
</testsuites>
</phpunit>
Test Database Setup
Tests use mocked WordPress functions defined in tests/bootstrap.php. No actual database connection is required for unit tests:
// Global mock storage
global $_wp_options, $_wp_transients;
$_wp_options = array();
$_wp_transients = array();
Environment variables for AI providers are set in the PHPUnit configuration:
<php>
<env name="GEMINI_API_KEY" value="test-key"/>
<env name="OPENAI_API_KEY" value="test-key"/>
<env name="ANTHROPIC_API_KEY" value="test-key"/>
<env name="SUPABASE_URL" value="https://test.supabase.co"/>
<env name="SUPABASE_KEY" value="test-key"/>
</php>
Test Structure
Test File Organization
tests/
├── bootstrap.php # PHPUnit bootstrap with WordPress mocks
├── Helpers/
│ ├── TestCase.php # Base test class with utilities
│ ├── MockAIProvider.php # AI provider test double
│ └── MockWooCommerce.php # WooCommerce simulation
├── AI/
│ └── ProvidersTest.php # AI provider unit tests
├── Search/
│ └── SemanticSearchTest.php # Semantic search tests
├── Chat/
│ └── MessageHandlerTest.php # Message handling tests
├── Integration/
│ └── WooCommerceTest.php # WooCommerce integration tests
└── E2E/
└── ChatFlowTest.php # End-to-end flow tests
Test Naming Conventions
Follow this pattern for test method names:
public function test_{method_or_feature}_{scenario}_{expected_outcome}()
Examples:
test_gemini_provider_initialization()test_gemini_provider_unavailable_without_key()test_rate_limit_simulation()
Base Test Classes
Extend WooAIChatbot\Tests\Helpers\TestCase for access to common utilities:
namespace WooAIChatbot\Tests\AI;
use WooAIChatbot\Tests\Helpers\TestCase;
class ProvidersTest extends TestCase {
protected function setUp(): void {
parent::setUp();
// Test-specific setup
}
protected function tearDown(): void {
parent::tearDown();
// Cleanup
}
}
Available helper methods in TestCase:
| Method | Purpose |
|---|---|
setEnv($name, $value) |
Set environment variable for test |
createWPError($code, $message) |
Create mock WP_Error |
assertIsWPError($actual) |
Assert value is WP_Error |
assertNotWPError($actual) |
Assert value is not WP_Error |
assertWPErrorCode($code, $error) |
Assert specific error code |
assertArrayHasKeys($keys, $array) |
Assert array has all keys |
getPrivateProperty($obj, $prop) |
Access private property |
callPrivateMethod($obj, $method) |
Call private method |
createTempFile($content) |
Create temporary file |
Running Tests
Composer Test Commands
# Run all tests
composer test
# Run specific test suite
composer test:unit
composer test:integration
composer test:e2e
# Run with coverage
composer test:coverage # HTML report
composer test:coverage:text # Console output
Individual Test Execution
# Run specific test file
./vendor/bin/phpunit tests/AI/ProvidersTest.php
# Run specific test method
./vendor/bin/phpunit --filter test_gemini_provider_initialization
# Run by group annotation
./vendor/bin/phpunit --group ai
./vendor/bin/phpunit --group woocommerce
Code Coverage
Coverage reports are generated to coverage/:
composer test:coverage
Output locations:
coverage/html/index.html- Interactive HTML reportcoverage/clover.xml- CI integration formatcoverage/junit.xml- JUnit XML format
Writing Tests
Unit Test Example
/**
* Test GeminiProvider initialization
*
* @covers \WooAIChatbot\AI\Providers\GeminiProvider::__construct
* @group ai
* @group providers
*/
public function test_gemini_provider_initialization() {
// Arrange
$this->setEnv( 'GEMINI_API_KEY', 'test-gemini-key' );
// Act
$provider = new GeminiProvider();
// Assert
$this->assertInstanceOf( GeminiProvider::class, $provider );
$this->assertTrue( $provider->is_available() );
$this->assertEquals( 'Google Gemini', $provider->get_provider_name() );
}
Integration Test Example
/**
* Test product context extraction
*
* @group integration
* @group woocommerce
*/
public function test_product_context_extraction() {
// Arrange
$product = MockWooCommerce::create_product(
array(
'id' => 123,
'name' => 'Premium Headphones',
'price' => 199.99,
'description' => 'High-quality wireless headphones',
'in_stock' => true,
)
);
// Act
$context = array(
'product_id' => $product->get_id(),
'name' => $product->get_name(),
'price' => $product->get_price(),
'description' => $product->get_description(),
'in_stock' => $product->is_in_stock(),
);
// Assert
$this->assertArrayHasKeys(
array( 'product_id', 'name', 'price', 'description', 'in_stock' ),
$context
);
$this->assertEquals( 'Premium Headphones', $context['name'] );
$this->assertTrue( $context['in_stock'] );
}
Mocking Strategies
MockAIProvider Usage:
// Success scenario
$mock_provider = new MockAIProvider(
array(
'response' => array(
'content' => 'Test response',
'tokens' => 5,
'model' => 'test-model',
),
)
);
$response = $mock_provider->generate_response( $messages );
$this->assertEquals( 'Test response', $response['content'] );
// Error scenario
$mock_provider = new MockAIProvider(
array(
'response' => $this->createWPError( 'rate_limit_exceeded', 'Rate limit exceeded' ),
)
);
$response = $mock_provider->generate_response( array() );
$this->assertIsWPError( $response );
// Call verification
$this->assertTrue( $mock_provider->was_called( 'generate_response' ) );
$this->assertEquals( 1, $mock_provider->get_call_count( 'generate_response' ) );
MockWooCommerce Usage:
// Create test products
MockWooCommerce::create_product( array(
'id' => 1,
'name' => 'Product 1',
'price' => 29.99,
));
// Add to cart
MockWooCommerce::add_to_cart( 1, 2 );
// Get cart
$cart = MockWooCommerce::get_cart();
// Reset between tests
MockWooCommerce::reset();
3. JavaScript Testing (Jest)
Setup
Jest Configuration
The jest.config.js configures TypeScript support with path aliases:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>/assets/src'],
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/assets/src/$1',
'^@admin/(.*)$': '<rootDir>/assets/src/admin/$1',
'^@chat/(.*)$': '<rootDir>/assets/src/chat/$1',
'^@components/(.*)$': '<rootDir>/assets/src/components/$1',
'^@hooks/(.*)$': '<rootDir>/assets/src/hooks/$1',
'^@utils/(.*)$': '<rootDir>/assets/src/utils/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};
Testing Library Integration
The jest.setup.js provides WordPress globals and configuration:
import '@testing-library/jest-dom';
// Mock WordPress global
global.wp = {
i18n: {
__: (text) => text,
_x: (text) => text,
_n: (single, plural, number) => (number === 1 ? single : plural),
},
hooks: {
addAction: jest.fn(),
addFilter: jest.fn(),
doAction: jest.fn(),
applyFilters: jest.fn(),
},
};
// Mock window.wooAIConfig
global.window.wooAIConfig = {
apiUrl: 'http://localhost/wp-json/woo-ai/v1',
nonce: 'test-nonce',
locale: 'en_US',
isRtl: false,
};
Running Tests
# Run all JavaScript tests
npm run test
# Watch mode for development
npm run test:watch
# Generate coverage report
npm run test:coverage
Writing Tests
Component Test Example
import { render, screen, fireEvent } from '@testing-library/react';
import { ChatMessage } from '@components/ChatMessage';
describe('ChatMessage', () => {
it('renders user message correctly', () => {
// Arrange
const message = {
id: '1',
role: 'user' as const,
content: 'Hello, I need help with shoes',
timestamp: new Date().toISOString(),
};
// Act
render(<ChatMessage message={message} />);
// Assert
expect(screen.getByText('Hello, I need help with shoes')).toBeInTheDocument();
expect(screen.getByRole('article')).toHaveClass('user-message');
});
it('renders assistant message with products', () => {
// Arrange
const message = {
id: '2',
role: 'assistant' as const,
content: 'Here are some shoes I found',
data: {
products: [
{ id: 1, name: 'Running Shoes', price: '$99.00' },
],
},
timestamp: new Date().toISOString(),
};
// Act
render(<ChatMessage message={message} />);
// Assert
expect(screen.getByText('Running Shoes')).toBeInTheDocument();
expect(screen.getByText('$99.00')).toBeInTheDocument();
});
});
Hook Test Example
import { renderHook, act } from '@testing-library/react';
import { useChat } from '@hooks/useChat';
describe('useChat', () => {
beforeEach(() => {
global.fetch = jest.fn();
});
it('initializes with empty messages', () => {
const { result } = renderHook(() => useChat());
expect(result.current.messages).toEqual([]);
expect(result.current.isLoading).toBe(false);
});
it('sends message and receives response', async () => {
// Mock API response
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({
content: 'Here are your results',
type: 'text',
}),
});
const { result } = renderHook(() => useChat());
await act(async () => {
await result.current.sendMessage('Show me laptops');
});
expect(result.current.messages).toHaveLength(2);
expect(result.current.messages[0].role).toBe('user');
expect(result.current.messages[1].role).toBe('assistant');
});
});
Service Test Example
import { ChatService } from '@utils/ChatService';
describe('ChatService', () => {
let chatService: ChatService;
beforeEach(() => {
chatService = new ChatService({
apiUrl: 'http://localhost/wp-json/woo-ai/v1',
nonce: 'test-nonce',
});
global.fetch = jest.fn();
});
it('sends chat message with correct headers', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ content: 'Response' }),
});
await chatService.send('Hello');
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost/wp-json/woo-ai/v1/chat',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json',
'X-WP-Nonce': 'test-nonce',
}),
})
);
});
it('handles API errors gracefully', async () => {
(global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'));
await expect(chatService.send('Hello')).rejects.toThrow('Network error');
});
});
4. E2E Testing (Playwright)
Setup
Playwright is included as a dependency for browser-based E2E testing:
# Install Playwright browsers
npx playwright install
Playwright Configuration
Create playwright.config.ts for WordPress E2E scenarios:
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
timeout: 30000,
use: {
baseURL: 'http://localhost:8080',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
{ name: 'firefox', use: { browserName: 'firefox' } },
{ name: 'webkit', use: { browserName: 'webkit' } },
],
});
Writing E2E Tests
Page Object Pattern
// e2e/pages/ChatWidgetPage.ts
import { Page, Locator } from '@playwright/test';
export class ChatWidgetPage {
readonly page: Page;
readonly chatButton: Locator;
readonly chatInput: Locator;
readonly sendButton: Locator;
readonly messagesContainer: Locator;
constructor(page: Page) {
this.page = page;
this.chatButton = page.locator('[data-testid="chat-widget-button"]');
this.chatInput = page.locator('[data-testid="chat-input"]');
this.sendButton = page.locator('[data-testid="send-button"]');
this.messagesContainer = page.locator('[data-testid="messages-container"]');
}
async openChat() {
await this.chatButton.click();
await this.page.waitForSelector('[data-testid="chat-input"]');
}
async sendMessage(message: string) {
await this.chatInput.fill(message);
await this.sendButton.click();
}
async waitForResponse() {
await this.page.waitForSelector('[data-testid="assistant-message"]');
}
async getLastMessage(): Promise<string> {
const messages = this.messagesContainer.locator('[data-testid="message"]');
const lastMessage = messages.last();
return lastMessage.textContent() ?? '';
}
}
Common Scenarios
// e2e/chat-flow.spec.ts
import { test, expect } from '@playwright/test';
import { ChatWidgetPage } from './pages/ChatWidgetPage';
test.describe('Chat Widget Flow', () => {
let chatWidget: ChatWidgetPage;
test.beforeEach(async ({ page }) => {
chatWidget = new ChatWidgetPage(page);
await page.goto('/shop');
});
test('opens chat widget and sends message', async ({ page }) => {
await chatWidget.openChat();
await chatWidget.sendMessage('Show me running shoes');
await chatWidget.waitForResponse();
const response = await chatWidget.getLastMessage();
expect(response).toContain('running shoes');
});
test('displays product cards in response', async ({ page }) => {
await chatWidget.openChat();
await chatWidget.sendMessage('I need a laptop under $1000');
await chatWidget.waitForResponse();
const productCards = page.locator('[data-testid="product-card"]');
await expect(productCards).toHaveCount.greaterThan(0);
});
});
5. Code Quality Tools
ESLint
Configuration
The .eslintrc.json extends recommended TypeScript and React rules:
{
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"react/react-in-jsx-scope": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}],
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
Commands
# Lint with auto-fix
npm run lint
# Check only (no modifications)
npm run lint:check
PHPCS
WordPress Standards
The phpcs.xml configuration enforces PHP syntax and security rules:
<ruleset name="WooAI Chatbot Pro">
<file>./woo-ai-chatbot-pro.php</file>
<file>./includes</file>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/tests/*</exclude-pattern>
<!-- Syntax and security -->
<rule ref="Generic.PHP.Syntax"/>
<rule ref="Squiz.PHP.Eval"/>
<rule ref="Generic.PHP.NoSilencedErrors"/>
</ruleset>
Commands
# Run PHPCS
npm run phpcs
# or
composer phpcs
# Auto-fix issues
npm run phpcs:fix
# or
composer phpcbf
TypeScript
Strict Mode Configuration
The tsconfig.json enables all strict checking options:
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true
}
}
Type Checking
# Run type check
npm run typecheck
6. Pre-commit Hooks (Husky)
Hook Configuration
Install Husky hooks:
npm run prepare
This runs husky install and sets up Git hooks in .husky/.
Lint-staged Setup
Add to package.json:
{
"lint-staged": {
"assets/src/**/*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"includes/**/*.php": [
"composer phpcs"
]
}
}
Pre-commit Hook
Create .husky/pre-commit:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run typecheck
npx lint-staged
npm run test -- --passWithNoTests
7. Test Data & Fixtures
Sample Data
Use MockWooCommerce to create consistent test products:
protected function createTestProducts(): array {
return array(
MockWooCommerce::create_product( array(
'id' => 1,
'name' => 'Running Shoes',
'price' => 129.99,
)),
MockWooCommerce::create_product( array(
'id' => 2,
'name' => 'Hiking Boots',
'price' => 189.99,
)),
);
}
Factory Patterns
class ProductFactory {
private static int $id = 0;
public static function create( array $overrides = array() ): MockProduct {
return MockWooCommerce::create_product( array_merge(
array(
'id' => ++self::$id,
'name' => 'Product ' . self::$id,
'price' => rand( 10, 500 ) + 0.99,
),
$overrides
));
}
public static function createMany( int $count ): array {
return array_map( fn() => self::create(), range( 1, $count ) );
}
}
Database Seeding
For integration tests requiring database state:
protected function seedTestData(): void {
global $_wp_options;
$_wp_options['woo_ai_settings'] = array(
'primary_provider' => 'gemini',
'fallback_providers' => array( 'openai', 'claude' ),
'temperature' => 0.7,
);
// Create test products
for ( $i = 1; $i <= 10; ++$i ) {
MockWooCommerce::create_product( array(
'id' => $i,
'name' => "Test Product {$i}",
'price' => $i * 10.00,
));
}
}
8. CI/CD Integration
GitHub Actions
Create .github/workflows/tests.yml:
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
php-tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.2', '8.3']
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run PHPUnit
run: composer test:coverage:text
- name: Run PHPCS
run: composer phpcs
js-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint:check
- name: Run tests
run: npm run test:coverage
- name: Build
run: npm run build
Deployment Gates
Tests must pass before deployment. Configure branch protection rules:
- Required checks:
php-tests,js-tests - Coverage thresholds: Fail if coverage drops below 80%
- No force pushes: Protect main branch
Coverage Reporting
Upload coverage to external services:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/clover.xml
fail_ci_if_error: true
Quick Reference
| Task | Command |
|---|---|
| Run all PHP tests | composer test |
| Run PHP unit tests | composer test:unit |
| Run PHP integration tests | composer test:integration |
| Run PHP E2E tests | composer test:e2e |
| PHP coverage report | composer test:coverage |
| Run all JS tests | npm run test |
| JS watch mode | npm run test:watch |
| JS coverage | npm run test:coverage |
| TypeScript check | npm run typecheck |
| ESLint | npm run lint |
| PHPCS | npm run phpcs |
| Fix PHPCS issues | npm run phpcs:fix |
Troubleshooting
Common Issues
PHPUnit bootstrap fails:
- Ensure
composer installcompleted successfully - Verify
.envfile exists (copied from.env.example)
Jest module resolution errors:
- Run
npm cito clean install dependencies - Verify path aliases in
jest.config.jsmatchtsconfig.json
Coverage below threshold:
- Add tests for uncovered branches
- Review
collectCoverageFromin Jest config for exclusions
Mock not resetting between tests:
- Call
MockWooCommerce::reset()insetUp() - Ensure parent
setUp()is called first