Search

Testing and Documenting a Statamic Addon: PHPUnit, CI and Docs That Sell

Testing and Documenting a Statamic Addon: PHPUnit, CI and Docs That Sell

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, so Addon::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 PreventsSavingStacheItemsToDisk trait when tests create entries, terms or globals, so fixtures don't pile up in tests/__fixtures__.
  • Toggle editions or config by overriding resolveApplicationConfiguration() in your TestCase. 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.png

Terminal showing ./vendor/bin/phpunit with 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.png

The 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:

  1. Create a fresh site: statamic new install-test
  2. Install your addon exactly as your README says, from a Git tag or Packagist, not from the path repository
  3. Follow your README step by step, as if you'd never seen the addon before
  4. 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.json constraints 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.

Also Read: Software Testing Vs Quality Assurance - Tooling

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

Previous: ← Part 3: Custom Fieldtypes with Vue 3 · Next: Part 5: The Marketplace Submission Guidelines →

TWT Staff

TWT Staff

Writes about Programming, tech news, discuss programming topics for web developers (and Web designers), and talks about SEO tools and techniques

Your experience on this site will be improved by allowing cookies Cookie Policy