Add PHPUnit testing framework and error checking documentation

This commit is contained in:
2025-04-22 22:42:11 +01:00
parent a17a574a7e
commit 5f598f0f7e
9 changed files with 500 additions and 5 deletions

61
.github/workflows/phpunit.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: PHPUnit Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: PHP ${{ matrix.php }} - WP ${{ matrix.wp }} - ${{ matrix.multisite && 'Multisite' || 'Single Site' }}
runs-on: ubuntu-latest
strategy:
matrix:
php: [ '7.4', '8.0' ]
wp: [ 'latest' ]
multisite: [ false, true ]
services:
mysql:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress_test
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup PHP
uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # v2.30.0
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, bcmath, soap, intl, gd, exif, iconv
coverage: none
- name: Install Composer dependencies
uses: ramsey/composer-install@83af392bf5f031813d25e6fe4cd626cdba9a2df6 # v2.2.0
- name: Install WordPress test suite
run: |
bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 ${{ matrix.wp }} ${{ matrix.multisite && 'true' || 'false' }}
- name: Run PHPUnit tests
run: |
if [ "${{ matrix.multisite }}" = "true" ]; then
WP_MULTISITE=1 composer test
else
composer test
fi

View File

@@ -0,0 +1,126 @@
# Error Checking and Feedback Loops
This document explains how to check for code quality issues and get feedback from automated tools in our development workflow.
## Table of Contents
* [Overview](#overview)
* [Local Error Checking](#local-error-checking)
* [CI/CD Feedback Loops](#cicd-feedback-loops)
* [Common Issues and Solutions](#common-issues-and-solutions)
* [Improving Code Quality](#improving-code-quality)
## Overview
Our development process includes multiple layers of error checking and feedback loops to ensure high code quality:
1. **Local Development**: Run linters and tests locally before committing
2. **Pull Request**: Automated checks run when you create or update a PR
3. **Code Review**: Human reviewers provide feedback on your code
4. **Continuous Integration**: Tests run in various environments to ensure compatibility
## Local Error Checking
### PHP Code Quality Checks
Run these commands locally to check for PHP code quality issues:
```bash
# Run all PHP code quality checks
npm run lint:php
# Run specific checks
npm run lint:phpcs # PHP CodeSniffer
npm run lint:phpstan # PHPStan static analysis
npm run lint:phpmd # PHP Mess Detector
```
### JavaScript/CSS Checks
```bash
# Run ESLint for JavaScript files
npm run lint:js
# Run stylelint for CSS files
npm run lint:css
```
### Running Tests Locally
```bash
# Run Cypress tests for single site
npm run test:playground:single
# Run Cypress tests for multisite
npm run test:playground:multisite
```
## CI/CD Feedback Loops
When you push code or create a pull request, several automated checks run:
### GitHub Actions Workflows
* **Code Quality**: Runs PHP CodeSniffer, PHPStan, and PHP Mess Detector
* **WordPress Tests**: Runs tests in WordPress environments
* **WordPress Playground Tests**: Runs tests in WordPress Playground environments
* **Tests - Run PHP compatibility and unit tests**: Checks compatibility with different PHP versions
### Third-Party Code Quality Services
* **CodeFactor**: Provides automated code reviews and quality grades
* **Codacy**: Analyzes code quality and security issues
* **SonarCloud**: Detects bugs, vulnerabilities, and code smells
## Common Issues and Solutions
### PHP CodeSniffer Issues
* **Indentation**: Use tabs for indentation in PHP files
* **Spacing**: Add spaces after commas, around operators, and after control structures
* **Naming Conventions**: Use snake_case for functions and variables in PHP
* **DocBlocks**: Add proper documentation for functions and classes
### PHPStan Issues
* **Undefined Variables**: Ensure all variables are defined before use
* **Type Errors**: Use proper type hints and return types
* **Null Checks**: Add null checks for variables that might be null
### JavaScript/CSS Issues
* **ESLint Errors**: Follow JavaScript best practices
* **Stylelint Errors**: Follow CSS best practices
* **Accessibility Issues**: Ensure UI elements are accessible
## Improving Code Quality
### Best Practices
* **Write Tests First**: Use test-driven development (TDD) when possible
* **Small PRs**: Keep pull requests small and focused on a single issue
* **Regular Commits**: Commit frequently with clear messages
* **Code Reviews**: Request code reviews from team members
* **Documentation**: Keep documentation up-to-date
### Using AI Assistants
AI assistants can help you understand and fix code quality issues:
1. Copy the error message or feedback
2. Paste it into your AI assistant chat
3. Ask for help understanding and fixing the issue
4. Apply the suggested fixes
5. Run the checks again to verify the issue is resolved
### Continuous Learning
* Review the code quality reports regularly
* Learn from feedback and improve your coding practices
* Stay updated on best practices for WordPress development
## Related Documentation
* [Testing Framework](Testing.md)
* [Code Review Guide](../.ai-workflows/code-review.md)
* [Architecture Overview](Architecture-Overview.md)

View File

@@ -12,6 +12,7 @@ This document explains how to use the testing framework for our plugin.
* [Writing Tests](#writing-tests)
* [CI/CD Integration](#cicd-integration)
* [Troubleshooting](#troubleshooting)
* [PHPUnit Tests](#phpunit-tests)
* [Future Improvements](#future-improvements)
## Overview
@@ -200,15 +201,56 @@ We have GitHub Actions workflows for running tests in CI/CD:
1. **Docker not running**: Make sure Docker is running before starting wp-env
2. **Port conflicts**: If ports 8888 or 8889 are in use, wp-env will fail to start
3. **wp-env not installed**: Run `npm install -g @wordpress/env` to install wp-env globally
4. **WordPress Playground not loading**: Check your network connection and try refreshing the page
5. **Tests failing**: Check the error messages in the console and fix the issues
### Error Checking and Feedback Loops
For detailed information on how to check for code quality issues and get feedback from automated tools, see the [Error Checking and Feedback Loops](Error-Checking-Feedback-Loops.md) documentation.
### Debugging
1. **Cypress debugging**: Use `cy.debug()` to pause test execution
2. **wp-env debugging**: Run `wp-env logs` to see WordPress logs
3. **GitHub Actions debugging**: Check the workflow logs for detailed error messages
## PHPUnit Tests
We use PHPUnit for unit testing PHP code. The tests are located in the `tests/phpunit` directory.
### Running PHPUnit Tests
```bash
# Run PHPUnit tests for single site
npm run test:phpunit
# Run PHPUnit tests for multisite
npm run test:phpunit:multisite
```
### Writing PHPUnit Tests
Here's an example of a PHPUnit test for the Multisite class:
```php
<?php
class MultisiteTest extends WP_UnitTestCase {
public function test_is_multisite_compatible() {
$multisite = new WP_Plugin_Starter_Template_For_AI_Coding\Multisite\Multisite();
$this->assertTrue($multisite->is_multisite_compatible());
}
public function test_get_network_sites() {
$multisite = new WP_Plugin_Starter_Template_For_AI_Coding\Multisite\Multisite();
$sites = $multisite->get_network_sites();
$this->assertIsArray($sites);
}
}
```
## Future Improvements
1. **PHPUnit tests**: Add unit tests for PHP code
2. **Performance tests**: Add performance testing
3. **Accessibility tests**: Add accessibility testing
1. **Performance tests**: Add performance testing
2. **Accessibility tests**: Add accessibility testing

159
bin/install-wp-tests.sh Executable file
View File

@@ -0,0 +1,159 @@
#!/usr/bin/env bash
if [ $# -lt 3 ]; then
echo "usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version] [skip-database-creation] [multisite]"
exit 1
fi
DB_NAME=$1
DB_USER=$2
DB_PASS=$3
DB_HOST=${4-localhost}
WP_VERSION=${5-latest}
SKIP_DB_CREATE=${6-false}
MULTISITE=${7-false}
WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib}
WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/}
download() {
if [ `which curl` ]; then
curl -s "$1" > "$2";
elif [ `which wget` ]; then
wget -nv -O "$2" "$1"
fi
}
if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
WP_BRANCH=${WP_VERSION%\-*}
WP_TESTS_TAG="branches/$WP_BRANCH"
WP_TESTS_TAG="tags/$WP_VERSION"
elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
WP_TESTS_TAG="branches/$WP_VERSION"
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
WP_TESTS_TAG="tags/${WP_VERSION%??}"
else
WP_TESTS_TAG="tags/$WP_VERSION"
fi
elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
WP_TESTS_TAG="trunk"
else
# http serves a single offer, whereas https serves multiple. we only want one
download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
if [[ -z "$LATEST_VERSION" ]]; then
echo "Latest WordPress version could not be found"
exit 1
fi
WP_TESTS_TAG="tags/$LATEST_VERSION"
fi
set -ex
install_wp() {
if [ -d $WP_CORE_DIR ]; then
return;
fi
mkdir -p $WP_CORE_DIR
if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
mkdir -p $WP_CORE_DIR
download https://wordpress.org/nightly-builds/wordpress-latest.zip $WP_CORE_DIR/wordpress-nightly.zip
unzip -q $WP_CORE_DIR/wordpress-nightly.zip -d $WP_CORE_DIR
rm $WP_CORE_DIR/wordpress-nightly.zip
else
if [ $WP_VERSION == 'latest' ]; then
local ARCHIVE_NAME='latest'
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
# https serves multiple offers, whereas http serves single.
download https://api.wordpress.org/core/version-check/1.7/ $WP_CORE_DIR/wp-latest.json
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
LATEST_VERSION=${WP_VERSION%??}
else
# otherwise, scan the releases and get the most up to date minor version of the major release
local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'`
LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $WP_CORE_DIR/wp-latest.json | sed 's/"version":"//' | head -1)
fi
if [[ -z "$LATEST_VERSION" ]]; then
local ARCHIVE_NAME="wordpress-$WP_VERSION"
else
local ARCHIVE_NAME="wordpress-$LATEST_VERSION"
fi
else
local ARCHIVE_NAME="wordpress-$WP_VERSION"
fi
download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $WP_CORE_DIR/wordpress.tar.gz
tar --strip-components=1 -zxmf $WP_CORE_DIR/wordpress.tar.gz -C $WP_CORE_DIR
rm $WP_CORE_DIR/wordpress.tar.gz
fi
download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
}
install_test_suite() {
# portable in-place argument for both GNU sed and Mac OSX sed
if [[ $(uname -s) == 'Darwin' ]]; then
local ioption='-i.bak'
else
local ioption='-i'
fi
# set up testing suite if it doesn't yet exist
if [ ! -d $WP_TESTS_DIR ]; then
# set up testing suite
mkdir -p $WP_TESTS_DIR
svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
fi
if [ ! -f wp-tests-config.php ]; then
download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
# remove all forward slashes in the end
WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
if [ "$MULTISITE" = "true" ]; then
sed $ioption "s:// define( 'WP_TESTS_MULTISITE', true );:define( 'WP_TESTS_MULTISITE', true );:" "$WP_TESTS_DIR"/wp-tests-config.php
fi
fi
}
install_db() {
if [ ${SKIP_DB_CREATE} = "true" ]; then
return 0
fi
# parse DB_HOST for port or socket references
local PARTS=(${DB_HOST//\:/ })
local DB_HOSTNAME=${PARTS[0]};
local DB_SOCK_OR_PORT=${PARTS[1]};
local EXTRA=""
if ! [ -z $DB_HOSTNAME ] ; then
if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
elif ! [ -z $DB_SOCK_OR_PORT ] ; then
EXTRA=" --socket=$DB_SOCK_OR_PORT"
elif ! [ -z $DB_HOSTNAME ] ; then
EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
fi
fi
# create database
mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
}
install_wp
install_test_suite
install_db

View File

@@ -32,12 +32,14 @@
},
"autoload": {
"psr-4": {
"WPALLSTARS\\PluginStarterTemplate\\": "includes/"
"WPALLSTARS\\PluginStarterTemplate\\": "includes/",
"WP_Plugin_Starter_Template_For_AI_Coding\\": "includes/"
}
},
"autoload-dev": {
"classmap": [
"includes/Admin/class-admin.php"
"includes/Admin/class-admin.php",
"tests/phpunit/"
]
},
"config": {

View File

@@ -17,6 +17,8 @@
"test:e2e:multisite": "npm run setup:multisite && sleep 5 && npm run test:multisite:headless",
"test:playground:single": "cypress run --spec cypress/e2e/playground-single-site.cy.js",
"test:playground:multisite": "cypress run --spec cypress/e2e/playground-multisite.cy.js",
"test:phpunit": "composer test",
"test:phpunit:multisite": "WP_MULTISITE=1 composer test",
"build": "./build.sh",
"lint:php": "composer run-script phpcs",
"lint:php:simple": "composer run-script phpcs:simple",

25
phpunit.xml.dist Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0"?>
<phpunit
bootstrap="tests/phpunit/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite name="plugin">
<directory prefix="test-" suffix=".php">./tests/phpunit/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">./includes</directory>
<exclude>
<directory suffix=".php">./vendor</directory>
<directory suffix=".php">./tests</directory>
<directory suffix=".php">./node_modules</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,22 @@
<?php
/**
* PHPUnit bootstrap file.
*
* @package WP_Plugin_Starter_Template_For_AI_Coding
*/
// Composer autoloader must be loaded before WP_PHPUNIT__DIR will be available.
require_once dirname( dirname( __DIR__ ) ) . '/vendor/autoload.php';
// Give access to tests_add_filter() function.
require_once getenv( 'WP_PHPUNIT__DIR' ) . '/includes/functions.php';
/**
* Manually load the plugin being tested.
*/
function _manually_load_plugin() {
require dirname( dirname( __DIR__ ) ) . '/plugin-toggle.php';
}
// Start up the WP testing environment.
require getenv( 'WP_PHPUNIT__DIR' ) . '/includes/bootstrap.php';

View File

@@ -0,0 +1,56 @@
<?php
/**
* Class MultisiteTest
*
* @package WP_Plugin_Starter_Template_For_AI_Coding
*/
/**
* Sample test case for the Multisite class.
*/
class MultisiteTest extends WP_UnitTestCase {
/**
* Test instance creation.
*/
public function test_instance() {
$multisite = new WP_Plugin_Starter_Template_For_AI_Coding\Multisite\Multisite();
$this->assertInstanceOf( 'WP_Plugin_Starter_Template_For_AI_Coding\Multisite\Multisite', $multisite );
}
/**
* Test is_multisite_compatible method.
*/
public function test_is_multisite_compatible() {
$multisite = new WP_Plugin_Starter_Template_For_AI_Coding\Multisite\Multisite();
$this->assertTrue( $multisite->is_multisite_compatible() );
}
/**
* Test get_network_sites method.
*/
public function test_get_network_sites() {
$multisite = new WP_Plugin_Starter_Template_For_AI_Coding\Multisite\Multisite();
// Mock the get_sites function if we're not in a multisite environment.
if ( ! function_exists( 'get_sites' ) ) {
$this->assertEquals( array(), $multisite->get_network_sites() );
} else {
$sites = $multisite->get_network_sites();
$this->assertIsArray( $sites );
}
}
/**
* Test initialize_hooks method.
*/
public function test_initialize_hooks() {
$multisite = new WP_Plugin_Starter_Template_For_AI_Coding\Multisite\Multisite();
// Call the method.
$multisite->initialize_hooks();
// Check if the action was added.
$this->assertEquals( 10, has_action( 'network_admin_menu', array( $multisite, 'add_network_menu' ) ) );
}
}