Our Reading Time addon works on our machine. That isn't good enough for the Statamic Marketplace any more. This post shows how to test a Statamic addon properly, then document it so customers and reviewers never have to guess.
Also Read: Prepare a WordPress Plugin for Release: readme & Plugin Check
Since September 2026, every product is reviewed against the Marketplace Submission Guidelines. Two of the 11 rules are directly about what this post covers:
- 06: Ship an installable, compatible release. Your release has to install cleanly and work on the versions you claim to support.
- 09: Document the product and provide support. Customers must be able to "install, configure, use, and maintain the product without guessing."
Tests prove the first one. Good docs prove the second. Let's do both.
Part A: Testing your addon
What make:addon already set up
When you scaffolded the addon in part 2, Statamic created a working test suite:
tests/
ExampleTest.php
TestCase.php
phpunit.xml
tests/TestCase.php extends Statamic's AddonTestCase:
<?php
namespace TheWebTier\ReadingTime\Tests;
use Statamic\Testing\AddonTestCase;
use TheWebTier\ReadingTime\ServiceProvider;
abstract class TestCase extends AddonTestCase
{
protected string $addonServiceProvider = ServiceProvider::class;
}
AddonTestCase is built on Orchestra Testbench, so every test boots a real Laravel app with Statamic and your addon loaded. It also:
- registers your addon from its
composer.json, soAddon::get('thewebtier/reading-time')works in tests - points the Stache at
tests/__fixtures__, so tests never touch a real site's content - disables Vite and Mix, so tests don't need built assets
Run the suite from inside the addon folder (that's why make:addon ran composer install there):
cd addons/thewebtier/reading-time
./vendor/bin/phpunit
Write real tests
Delete ExampleTest.php and add tests/ReadingTimeTest.php. We'll test three layers: the plain calculator, the modifier, and the tag rendered through Antlers.
<?php
namespace TheWebTier\ReadingTime\Tests;
use PHPUnit\Framework\Attributes\Test;
use Statamic\Facades\Antlers;
use Statamic\Modifiers\Modify;
use TheWebTier\ReadingTime\Support\ReadingTime;
class ReadingTimeTest extends TestCase
{
#[Test]
public function it_counts_words_in_html()
{
$calculator = new ReadingTime(200);
$this->assertSame(2, $calculator->words('<p>One</p><p>Two</p>'));
}
#[Test]
public function it_rounds_up_to_the_nearest_minute()
{
$calculator = new ReadingTime(200);
$this->assertSame(0, $calculator->minutes(''));
$this->assertSame(1, $calculator->minutes(str_repeat('word ', 10)));
$this->assertSame(2, $calculator->minutes(str_repeat('word ', 201)));
}
#[Test]
public function it_reads_raw_bard_values()
{
$bard = [
['type' => 'paragraph', 'content' => [
['type' => 'text', 'text' => 'Hello there world'],
]],
];
$this->assertSame(3, (new ReadingTime)->words($bard));
}
#[Test]
public function the_modifier_uses_the_label_setting()
{
$text = str_repeat('word ', 450);
$this->assertSame('3 min read', (string) Modify::value($text)->readingTime());
$this->assertSame(3, Modify::value($text)->readingTime('raw')->fetch());
}
#[Test]
public function the_tag_exposes_minutes_and_words_as_a_pair()
{
$output = (string) Antlers::parse(
'{{ reading_time field="body" }}{{ minutes }}|{{ words }}{{ /reading_time }}',
['body' => str_repeat('word ', 250)]
);
$this->assertSame('2|250', $output);
}
}
Some tips for addon tests:
- Test edge cases, not just the demo. Empty values, HTML, Bard arrays, very long content. Guideline 05 is literally "work beyond the demo's happy path", and a reviewer will try the things you didn't.
Modify::value($x)->yourModifier()runs a modifier exactly as templates do.Antlers::parse($template, $data)renders a tag through the real parser. It's the closest you'll get to a front-end test without a browser.- Test Control Panel routes with the
->assertInertia()macro on responses, if your addon has CP pages. - Use the
PreventsSavingStacheItemsToDisktrait when tests create entries, terms or globals, so fixtures don't pile up intests/__fixtures__. - Toggle editions or config by overriding
resolveApplicationConfiguration()in yourTestCase. For example,$app['config']->set('statamic.editions.pro', true);tests Pro-only behaviour.
If you use update scripts (covered in part 7), Statamic 6.3 and later includes a RunsUpdateScripts trait with $this->runUpdateScript(YourScript::class).
📸 SCREENSHOT TO ADD ·
04-phpunit-green.pngTerminal showing
./vendor/bin/phpunitwith all 5 tests passing.Alt text: "PHPUnit test suite passing for a Statamic addon"
Run tests on every push with GitHub Actions
Your compatibility claim, "PHP 8.3+, Laravel 12 or 13, Statamic 6", is only true if you test it. Add .github/workflows/tests.yml. This is the matrix from Statamic's own testing docs:
name: Test Suite
on:
push:
pull_request:
jobs:
php_tests:
strategy:
matrix:
php: [8.3, 8.4, 8.5]
laravel: [12.*, 13.*]
os: [ubuntu-latest]
name: ${{ matrix.php }} - ${{ matrix.laravel }}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update
composer install --no-interaction
- name: Run PHPUnit
run: vendor/bin/phpunit
That's six combinations on every push and pull request. Add a status badge to your README and buyers can see at a glance that it's maintained.
📸 SCREENSHOT TO ADD ·
04-github-actions-matrix.pngThe GitHub Actions run page showing all six PHP × Laravel jobs green.
Alt text: "GitHub Actions matrix testing a Statamic addon across PHP and Laravel versions"
Also test the thing customers actually install
Unit tests don't catch a missing resources/dist or a dependency you forgot to declare. Before each release, do a clean install test:
- Create a fresh site:
statamic new install-test - Install your addon exactly as your README says, from a Git tag or Packagist, not from the path repository
- Follow your README step by step, as if you'd never seen the addon before
- Check the Control Panel with the Vite dev server off
This catches most of what guideline 06 lists as unacceptable: "missing globals or assets; references to local paths; undeclared dependencies."
Also Read: WordPress Development and PHP
Part B: Documentation that passes review (and sells)
Your README is your install guide, your support deflector and your sales page all at once. Statamic's guidelines list what's expected: "installation instructions, working examples, configuration guidance, changelog, support channel". They also list what's not acceptable: "generic boilerplate, dead support links, undocumented dependencies".
The README that make:addon generates ("Reading Time is a Statamic addon that does something pretty neat") is exactly the kind of boilerplate that fails. Replace all of it.
Also Read: WordPress Development Guide
A README structure that works
# Reading Time for Statamic
> Show readers how long a post takes to read, and show editors how long they're writing.
## Requirements
- PHP 8.3+
- Laravel 12 or 13
- Statamic 6
## Installation
```bash
composer require thewebtier/reading-time
```
## Configuration
Go to **Tools → Addons → Reading Time** and set words per minute and the label.
## Usage
### Modifier
{{ content | reading_time }} → "4 min read"
### Tag
{{ reading_time field="content" }}
### Blade
<s:reading_time field="content" />
### Fieldtype
Add a "Reading Time Textarea" field to any blueprint…
## Upgrading
See CHANGELOG.md.
## Support
Open an issue: https://github.com/thewebtier/statamic-reading-time/issues
## License
MIT
Things that make a real difference:
- Show Antlers and Blade. Plenty of Statamic sites use Blade now.
- Be exact about requirements. They must match your
composer.jsonconstraints and your CI matrix. - Use real screenshots. Guideline 10 says listing images must come from the actual product, and mockups presented as features count as misrepresentation.
- Check every link. A dead support link is specifically listed as unacceptable.
Keep a changelog
Use the Keep a Changelog format and semantic versioning:
# Changelog
## v1.0.0 - 2026-09-23
### Added
- `reading_time` modifier with a configurable label
- `reading_time` tag (single and pair)
- `reading_time_textarea` fieldtype with a live word count
- Control Panel settings for words per minute and label
You'll paste each entry into your GitHub release notes too. The Marketplace shows release notes on your addon's page, so write them for customers, not for yourself.
Declare everything you bundle
Guideline 08 says you must "have permission to distribute everything included". If your addon ships a font, an icon set, a JavaScript library or images, list them with their licences in the README (or a THIRD_PARTY.md). Keep any attribution the licence requires.
Also Read: Top 8 Mobile Testing Tooling Tools - The Web Tier
Declare external requests
If your addon calls an external API, sends telemetry or loads remote scripts, say so in the README. Guideline 07 lists "hidden tracking" as a reason for rejection, and "documented external requests" as the acceptable version.
Bonus: ship AI guidelines with your addon
Lots of Statamic developers now build with an AI coding assistant and Laravel Boost. Boost looks for a file at resources/boost/guidelines/core.blade.php in every installed package and loads it into the assistant's context when someone runs php artisan boost:install. Statamic itself ships one.
Add your own and assistants will use your addon correctly instead of guessing:
@verbatim
## Reading Time (thewebtier/reading-time)
This Statamic addon estimates how long content takes to read.
- Modifier: `{{ content | reading_time }}` returns the label from the addon settings, e.g. "4 min read". `{{ content | reading_time:raw }}` returns an integer.
- Tag: `{{ reading_time field="content" }}` returns minutes. As a pair it exposes `{{ minutes }}` and `{{ words }}`. Optional `wpm` parameter overrides the setting.
- Blade: `<s:reading_time field="content" />` or `{{ Statamic::modify($content)->readingTime() }}`.
- Fieldtype: `reading_time_textarea`, a textarea with a live word count. Config option `target_minutes`.
- Settings live in the Control Panel under Tools → Addons → Reading Time (`words_per_minute`, `label`). Don't hard-code reading speeds.
@endverbatim
Two details that matter:
- Wrap it in
@verbatim. The file is rendered as Blade, so Antlers'{{ }}would otherwise be treated as Blade echo tags. - Keep it short and specific. Boost adds guidelines to the assistant's context in every session, so write rules and working snippets, not marketing copy.
For longer, on-demand instructions, Boost also supports package skills at resources/boost/skills/{skill-name}/SKILL.md.
FAQ
How do I run tests for a Statamic addon?
From inside the addon folder, run ./vendor/bin/phpunit. The scaffolded TestCase extends Statamic\Testing\AddonTestCase, which boots Laravel, Statamic and your addon for every test.
Also Read: Testing Filament v5 Plugins with Pest & Testbench
Do I need a full Statamic site to test my addon?
No. AddonTestCase uses Orchestra Testbench to boot a throwaway app. You still need a clean install test on a real site before each release, to catch packaging problems.
Which PHP and Laravel versions should I test against for Statamic 6?
Statamic 6 needs PHP 8.3+ and Laravel 12+. Statamic's own example CI matrix covers PHP 8.3, 8.4 and 8.5 against Laravel 12 and 13.
What should a Statamic addon README include?
Requirements, installation, configuration, usage examples (Antlers and Blade), upgrade notes or a changelog link, a working support link and the licence. Screenshots must show the real product.
Further reading
- Testing in Addons (Statamic 6 docs)
- Orchestra Testbench
- Laravel Boost: third-party package guidelines
- Keep a Changelog and Semantic Versioning
Previous: ← Part 3: Custom Fieldtypes with Vue 3 · Next: Part 5: The Marketplace Submission Guidelines →
