Ava CMS includes a lightweight, zero-dependency test framework for verifying core functionality. Tests are designed for maintainers and contributors working on the CMS itself.
This page documents the test framework exactly as it exists in the current Ava CMS codebase:
- The runner is implemented in
core/Testing/. - The CLI entrypoint is
./ava test(implemented incore/Cli/Application.php). - Tests live in
core/tests/.
Running Tests
Run the test suite from your project root:
./ava test
Ava CMS Test Suite ────────────────────────────────────────────────── StrTest ✓ slug converts to lowercase ✓ slug replaces spaces with separator ✓ starts with returns true for match ... ParserTest ✓ parse extracts frontmatter and content ✓ parse handles multiple frontmatter fields ... ────────────────────────────────────────────────── Tests: 383 passed (70ms)
Command Synopsis
./ava test [filter] [-q|--quiet] [--release] [-v|--verbose]
Supported flags/arguments:
filter(optional): a single string used to filter the run. The runner first matches the class name (case-insensitive substring); if the class matches, the same filter is applied to method names to narrow which tests run within that class.-q,--quiet: reduce output.--release: include release-readiness tests undercore/tests/Release/.-v,--verbose: accepted by the CLI, but currently does not change output (reserved for a future verbose mode).
Quiet Mode
Run tests with minimal output (header + summary only):
./ava test --quiet
./ava test -q
Ava CMS Test Suite ────────────────────────────────────────────────── Tests: 383 passed (60ms)
Useful for CI/CD pipelines or when you just want to know if tests pass.
Test Structure
Tests live in core/tests/ and are organised by component:
core/tests/
├── Admin/
│ ├── AuthTest.php # Admin authentication
│ ├── ContentSecurityTest.php # Content Security Policy rules
│ ├── DebugTest.php # Debug configuration and logging
│ └── MediaUploaderTest.php # Media upload handling
├── Config/
│ └── ConfigTest.php # Configuration access patterns
├── Content/
│ ├── IndexerRoutesTest.php # Indexer route generation
│ ├── ItemTest.php # Content item value object
│ ├── ParserTest.php # Markdown/YAML parser
│ └── QueryTest.php # Content query builder
├── Core/
│ └── UpdaterTest.php # Updater behavior
├── Fields/
│ ├── BasicFieldsTest.php # Basic field types
│ ├── ComplexFieldsTest.php # Complex field types
│ ├── FieldRegistryTest.php # Field registry
│ ├── SystemFieldsTest.php # System fields
│ ├── TextFieldsTest.php # Text fields
│ └── ValidationResultTest.php # Field validation result
├── Http/
│ ├── HttpsEnforcementTest.php # HTTPS/localhost detection
│ ├── RequestTest.php # HTTP request handling
│ └── ResponseTest.php # HTTP response building
├── Plugins/
│ └── HooksTest.php # Action/filter hook system
├── Rendering/
│ ├── ErrorPagesTest.php # Error page rendering
│ ├── MarkdownTest.php # CommonMark rendering
│ └── RawHtmlTest.php # Raw HTML handling
├── Routing/
│ ├── RouteMatchTest.php # Route match value object
│ └── RouterTest.php # Router behavior
├── Shortcodes/
│ └── EngineTest.php # Shortcode processing
├── Release/
│ └── ReleaseChecksTest.php # Release-readiness checks (only with --release)
└── Support/
├── ArrTest.php # Array utilities
├── PathTest.php # Path utilities
├── StrTest.php # String utilities
└── UlidTest.php # ULID generation
Writing Tests
Tests are plain PHP classes discovered by filename + method name conventions.
Recommended approach: extend Ava\Testing\TestCase, which provides assertions and an injected $app instance.
Example:
<?php
declare(strict_types=1);
namespace Ava\Tests\Support;
use Ava\Support\Str;
use Ava\Testing\TestCase;
final class StrTest extends TestCase
{
public function testSlugConvertsToLowercase(): void
{
$this->assertEquals('hello-world', Str::slug('Hello World'));
}
public function testSlugRemovesSpecialCharacters(): void
{
$this->assertEquals('hello-world', Str::slug('Hello, World!'));
}
}
Test Discovery
The runner discovers tests by scanning core/tests/ recursively:
- Files must end with
Test.php - The runner looks for a
namespace ...;and aclass Namein the file and constructs the class name from those - All
publicmethods whose name starts withtestare executed - Classes must be instantiable with no constructor arguments
- Abstract classes are skipped
Notes / quirks:
- A test class does not technically have to extend
TestCaseto run, but then you won’t have Ava CMS’s assertion helpers or the$appinjection. - If you define
setUp()/tearDown(), define them aspublic. The runner calls them directly; non-public visibility will cause an error. - If
setUp()throws, that test is recorded as a failure and the test body is not run. - If
tearDown()throws, the error is ignored and does not fail the test.
Available Assertions
| Assertion | Description |
|---|---|
assertTrue($value) |
Assert value is true |
assertFalse($value) |
Assert value is false |
assertEquals($expected, $actual) |
Assert values are equal (==) |
assertSame($expected, $actual) |
Assert values are identical (===) |
assertNotSame($expected, $actual) |
Assert values are not identical |
assertNotEquals($expected, $actual) |
Assert values differ |
assertNull($value) |
Assert value is null |
assertNotNull($value) |
Assert value is not null |
assertInstanceOf($class, $object) |
Assert object is instance of class |
assertIsArray($value) |
Assert value is an array |
assertIsString($value) |
Assert value is a string |
assertArrayHasKey($key, $array) |
Assert array has key |
assertContains($needle, $haystack) |
Assert array contains value |
assertCount($expected, $array) |
Assert array has count |
assertEmpty($value) |
Assert value is empty |
assertNotEmpty($value) |
Assert value is not empty |
assertStringContains($needle, $haystack) |
Assert string contains substring |
assertStringNotContains($needle, $haystack) |
Assert string does not contain substring |
assertStringStartsWith($prefix, $string) |
Assert string starts with prefix |
assertStringEndsWith($suffix, $string) |
Assert string ends with suffix |
assertMatchesRegex($pattern, $string) |
Assert string matches regex |
assertThrows($exception, $callback) |
Assert callback throws exception |
assertGreaterThan($expected, $actual) |
Assert actual > expected |
assertLessThan($expected, $actual) |
Assert actual < expected |
assertGreaterThanOrEqual($expected, $actual) |
Assert actual ≥ expected |
assertLessThanOrEqual($expected, $actual) |
Assert actual ≤ expected |
assertIsInt($value) |
Assert value is an integer |
assertIsBool($value) |
Assert value is a boolean |
assertArrayEquals($expected, $actual) |
Assert arrays equal after key-sorting |
Skipping Tests
Skip a test conditionally:
public function testRequiresExtension(): void
{
if (!extension_loaded('igbinary')) {
$this->skip('Requires igbinary extension');
}
// Test code here
}
markSkipped($reason) is an alias of skip($reason).
Skipped tests are counted as skipped (not failed).
What Counts as a Failure?
- Any
AssertionFailedException(thrown by Ava CMS assertions) marks the test as failed. - Any other uncaught
Throwableinside a test method also marks it as failed.
Failures are listed at the end with the thrown message and file/line.
Release Test Mode
By default, tests under core/tests/Release/ are skipped.
To include them:
./ava test --release
These tests are intended for maintainers preparing a public release. See Releasing.
Test Philosophy
The test suite focuses on unit testing core utilities that have no external dependencies:
- Support classes (
Str,Arr,Path,Ulid) - Pure functions - Value objects (
Item,RouteMatch,Request,Response) - Immutable data - Parsing (
Parser) - Frontmatter/Markdown extraction - Hooks system - Filter/action registration and execution
- Shortcodes - Tag parsing and callback execution
- Markdown - CommonMark rendering behaviour
- Config access - Dot-notation array access patterns
Classes that require full Application context can still be tested: when you extend TestCase, Ava CMS injects an Ava\Application instance as $this->app.
However, Ava CMS’s test suite is still intentionally biased toward fast, deterministic tests.
Continuous Integration
For CI pipelines, the test command returns appropriate exit codes:
./ava test
echo $? # 0 = all passed, 1 = failures
Example GitHub Actions workflow:
- name: Run tests
run: ./ava test