Skip to content

feat(spec): init spec plugin#235

Draft
roxblnfk wants to merge 2 commits into
1.xfrom
plugin-spec
Draft

feat(spec): init spec plugin#235
roxblnfk wants to merge 2 commits into
1.xfrom
plugin-spec

Conversation

@roxblnfk

@roxblnfk roxblnfk commented Jun 23, 2026

Copy link
Copy Markdown
Member

What was changed

Adds a new testo/spec plugin that blends BDD, Spec-Driven and TDD: you write the behaviour a test proves right next to it, and Testo turns the suite into living documentation — and can run the tests in the order the spec describes.

Write the spec next to the test

#[Spec] carries the story; #[SpecHeader] carries the heading and number (on a class it opens a numbered section, on a method it titles/pins an item):

use Testo\Spec;
use Testo\Spec\SpecHeader;
use Testo\Test;

#[Test]
#[SpecHeader('5', 'Checkout')]   // class = section "5. Checkout"
final class CheckoutTest
{
    #[Spec(story: <<<'MD'
        **As a** customer
        **I want** the cart total to include tax
        **so that** the price I pay matches the price I see.
        MD, tags: ['checkout', 'JIRA-128'])]
    public function totalIncludesTax(): void   // -> item 5.1
    {
        // ...assertions that prove the story...
    }

    #[Test]
    #[Spec(story: 'A valid coupon lowers the total.')]
    #[SpecHeader(title: 'Coupon applies')]      // overrides the item title, still auto-numbered -> 5.2
    public function couponApplies(): void {}
}

Even with no plugin registered, each fragment is published to the spec.md messenger channel, so it travels with that test's output.

Generate the document

Register the plugin and either pass --spec / --spec-dir=<dir> or set collect: true:

use Testo\Spec\SpecPlugin;

// in testo.php — ApplicationConfig::$plugins or a SuiteConfig::$plugins
new SpecPlugin(outputDir: __DIR__ . '/docs/specs'),
vendor/bin/testo --spec                 # generate into the configured dir
vendor/bin/testo --spec-dir=docs/specs  # ...or override the dir

The run is rendered into a single ordered spec.md:

# 5. Checkout

## 5.1 totalIncludesTax

**As a** customer
**I want** the cart total to include tax
**so that** the price I pay matches the price I see.

_Tags: `checkout` `JIRA-128`_

## 5.2 Coupon applies

A valid coupon lowers the total.

Sections are sorted by number; items are auto-numbered {section}.{n}; colliding numbers get a (1)/(2) suffix; fragments with no section number fall into an # Uncategorized tail (bulleted when they have a header, plain paragraphs otherwise).

Run tests in spec order

By default the plugin also reorders execution to match the document — Test Cases run in section-number order, tests in item-number order, unnumbered ones last. Turn it off with new SpecPlugin(reorder: false).

Under the hood

  • Reordering is done with TestSuiteRunInterceptor / TestCaseRunInterceptor, backed by two new sort() methods on core CaseDefinitions / TestDefinitions.
  • Generation runs on TestSuiteFinished, so it works whether the plugin is application-wide or per-suite.
  • Monorepo wiring (composer.json, testo.php, release-please, resources/version.json, split-publish.yml) so the plugin ships as its own mirror repo on release.

Why?

Projects that want SDK/framework-grade test workflows need their tests to double as the spec. This gives Testo a first-class spec-driven mode: the specification lives in the tests, the generated spec.md stays in sync by construction, and execution order follows the spec numbering.

Checklist

  • Tested
    • Tested manually
    • Unit tests added
  • Documentation

🤖 Generated with Claude Code

@roxblnfk roxblnfk requested a review from a team as a code owner June 23, 2026 11:25
Numbered headers now read `#[SpecHeader('5.2', 'My title')]` (number first,
matching the constructor); unnumbered ones keep the named `title:` form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@roxblnfk roxblnfk marked this pull request as draft June 23, 2026 12:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant