phpunit/phpunit-skeleton-generator
PHPUnit Skeleton Generator creates skeleton test classes from existing production code and skeleton class stubs from test cases, helping PHP developers bootstrap PHPUnit tests quickly with less boilerplate.
The skeleton generator is a CLI tool with two directions:
App\Services\InvoiceCalculator, emit a InvoiceCalculatorTest with @covers annotations and a stub test method per public method.Minimal setup:
# Composer (preferred if your PHP is old enough for the constraint — see Gotchas)
composer require --dev phpunit/phpunit-skeleton-generator
# or the PHAR, which sidesteps dependency resolution entirely
curl -O https://phar.phpunit.de/phpunit-skeleton-generator.phar
chmod +x phpunit-skeleton-generator.phar
Verify and read the actual option names before scripting anything — the CLI changed shape between 1.x (--test -- Class File) and 2.x (generate-test / generate-class subcommands), and the README on GitHub tracks main, not the release you installed:
vendor/bin/phpunit-skelgen --version
vendor/bin/phpunit-skelgen --help
vendor/bin/phpunit-skelgen help generate-test
First use case — you just wrote a chunky service class and want a scaffold instead of a blank file:
vendor/bin/phpunit-skelgen generate-test \
--class 'App\Services\InvoiceCalculator' \
--class-file app/Services/InvoiceCalculator.php \
--test-class 'Tests\Unit\Services\InvoiceCalculatorTest' \
--test-class-file tests/Unit/Services/InvoiceCalculatorTest.php
If your build uses the 1.x form, the equivalent is:
vendor/bin/phpunit-skelgen --test -- 'App\Services\InvoiceCalculator' \
app/Services/InvoiceCalculator.php \
'Tests\Unit\Services\InvoiceCalculatorTest' \
tests/Unit/Services/InvoiceCalculatorTest.php
Run it against a class you already know is green, then diff the output against what you'd have written by hand. That diff tells you exactly how much post-processing your project needs — which for Laravel is usually the base class and nothing else.
Feature vs. unit split. The generator has no concept of Laravel's Tests\Feature / Tests\Unit namespaces or the Tests\TestCase base class. Pass both the test FQCN and the target path explicitly every time, and treat the namespace argument as mandatory rather than inferred:
# Unit test for a value object
vendor/bin/phpunit-skelgen generate-test \
--class 'App\Support\Money' \
--class-file app/Support/Money.php \
--test-class 'Tests\Unit\Support\MoneyTest' \
--test-class-file tests/Unit/Support/MoneyTest.php
# Feature test for a controller — generated against Tests\TestCase afterwards
vendor/bin/phpunit-skelgen generate-test \
--class 'App\Http\Controllers\InvoiceController' \
--class-file app/Http/Controllers/InvoiceController.php \
--test-class 'Tests\Feature\Http\InvoiceControllerTest' \
--test-class-file tests/Feature/Http/InvoiceControllerTest.php
Test-first / outside-in. The reverse direction is the more interesting one in a TDD loop: write a Tests\Feature\... test that calls methods you haven't written yet, then:
vendor/bin/phpunit-skelgen generate-class \
--class 'App\Services\ProrationCalculator' \
--class-file app/Services/ProrationCalculator.php \
--test-class 'Tests\Unit\Services\ProrationCalculatorTest' \
--test-class-file tests/Unit/Services/ProrationCalculatorTest.php
You get a class with the right FQCN, the right method signatures, and throw new \RuntimeException or empty bodies — enough for the test to fail with "not implemented" rather than "class not found". That distinction matters: a fatal autoload error tells you nothing about the contract, an assertSame mismatch does.
Wrap it in a composer script. Nobody remembers six options:
{
"scripts": {
"skelgen:test": "phpunit-skelgen generate-test --class-file",
"skelgen:class": "phpunit-skelgen generate-class --test-class-file"
}
}
Or a bin/skelgen shell wrapper that takes a single file path, derives the PSR-4 namespace from composer.json's autoload map, and infers tests/Unit/... vs tests/Feature/... by whether the class lives under App\Http. Twenty lines of PHP, and it's the difference between using the tool and abandoning it.
Normalize immediately. Whatever the generator emits, pipe it through your project's conventions before you start editing:
# swap the base class for Laravel's, add the trait you always add
sed -i 's/extends \\PHPUnit\\Framework\\TestCase/extends \\Tests\\TestCase/' \
tests/Feature/Http/InvoiceControllerTest.php
vendor/bin/php-cs-fixer fix tests/Feature/Http/InvoiceControllerTest.php
Better: capture the generator's raw output as a fixture, apply your own nikic/php-parser pass, and print the result. That's the extension point — the package is small enough to fork and rewrite than to fight.
Pair with native tooling, don't replace it. php artisan make:test InvoiceControllerTest --unit already gives you the correct namespace, the correct base class, and the correct directory. Use the generator for the method-level scaffolding (one stub per public method, @covers, typed mock setup) and artisan/IDE for the file-level scaffolding. When both are used, the generator's output is the thing you delete 60% of.
It is effectively abandoned. The repository is archived and the last releases pin phpunit/phpunit to the 5.x era. Installing on PHP 8 with PHPUnit 10/11 will fail dependency resolution. Your options are the PHAR (self-contained, runs on old PHP only), --ignore-platform-reqs (rarely works — the code uses PHPUnit internals that no longer exist), or a community fork. Budget for this before you sell the tool to a team.
It does not use Composer's autoloader. The generator parses the file you hand it with nikic/php-parser and reflects on it in isolation. If the class under test extends a base class, implements a Laravel interface, or uses a trait that the generator needs to resolve to build signatures, the output degrades — missing methods, missing @covers entries, sometimes a fatal. Pass --bootstrap vendor/autoload.php (or require your bootstrap/app.php), and if that still fails, flatten the dependency chain for the duration of the generation.
Namespace inference will be wrong for Laravel. The tool's default mapping assumes a src/ → PSR-4-root layout. app/ → App\ → tests/ → Tests\ is not that layout. Always pass --test-class and --test-class-file; never let it guess.
Silent overwrite. If the target file exists, several versions of the tool will clobber it without a prompt. Run git status after, or wrap the call in [ -f "$TARGET" ] && exit 1. This is the single most common way people lose an hour.
Output goes to stdout unless you name a file. If you omit --test-class-file, you get the source on stdout and nothing on disk. That's actually the safer mode — redirect into a temp file, review, then move.
Interfaces, abstract classes, and traits produce nonsense. An interface has no bodies, so you get an empty class; an abstract class yields stubs for methods that may already be implemented upstream. Filter your input: only run the generator on concrete, final-ish classes you actually intend to test at the unit level.
Constructor and typed-property setup is not generated. The skeleton has no setUp(), no promotion of constructor arguments, no mock wiring for typed parameters. Expect to write that first block of every generated test by hand — which is precisely the part you wanted help with. Set expectations accordingly when pitching this to reviewers.
No --dry-run, no dry-run diffing, no verbose mode. Debug by running php -d display_errors=1 vendor/bin/phpunit-skelgen ... and reading the parse exception, which usually names the file and line in the analyzed class, not the tool. If the tool exits 0 with empty output, the class name didn't match what the parser found — check case sensitivity and the namespace declaration against the path.
The real extension point is the codebase, not config. There is no plugin system, no config file, no service container. If the generated skeleton doesn't match your house style, clone the repo, edit the SkeletonGenerator classes' string templates, and ship your fork as a private VCS dependency. Anything more elaborate is wasted effort on a repo that stopped receiving commits years ago.
Modern alternatives, honestly: for class → test, PhpStorm's "Create Test" intention and php artisan make:test cover the file-level case with correct Laravel conventions; Pest's php artisan pest:test does the same for Pest suites. For test → class there is still no good drop-in, which is the one reason this package remains worth understanding — and, if you need it, worth a small internal reimplementation using nikic/php-parser directly rather than resurrecting the dependency.
How can I help you explore Laravel packages today?