Speeding Up Laravel Tests with tmpfs (MySQL in RAM)
If your Laravel test suite is painfully slow and does a lot of database work (multi-tenancy, migrations, seeding), the bottleneck is almost…
This guide shows you how to test Statamic addons effectively. Testing addons is different from testing regular PHP packages because they depend on Statamic's core functionality, which often requires a full application environment.
Unlike regular PHP packages, Statamic addons operate within a complex ecosystem:
The challenge? Many of these features require a full Statamic installation to function. You can't just mock everything and call it a day.
Let's start with the basics. Every addon should have at least a smoke test to ensure it loads correctly.
First, create a tests directory in your addon:
your-addon/
├── src/
├── resources/
├── tests/
│ ├── Unit/
│ ├── Feature/
│ └── TestCase.php
├── composer.json
└── phpunit.xml
Create a phpunit.xml file in your addon root. This tells PHPUnit how to run your tests:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
backupGlobals="false"
bootstrap="tests/bootstrap.php"
colors="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
</phpunit>
Create tests/bootstrap.php to ensure your test classes are autoloaded:
<?php
// Register test namespace
$loader = require __DIR__ . '/../../../vendor/autoload.php';
$loader->addPsr4('YourVendor\\YourAddon\\Tests\\', __DIR__);
This step is crucial! Without it, PHPUnit won't find your test classes.
Update your addon's composer.json:
{
"require-dev": {
"orchestra/testbench": "^9.0",
"mockery/mockery": "^1.6",
"phpunit/phpunit": "^10.0"
}
}
Important: Install these in your main Laravel project, not just the addon:
# If using Laravel Sail
./vendor/bin/sail composer require --dev orchestra/testbench mockery/mockery
# Regular Composer
composer require --dev orchestra/testbench mockery/mockery
Let's write a basic test to ensure your addon loads correctly:
<?php
namespace YourVendor\YourAddon\Tests\Unit;
use PHPUnit\Framework\TestCase;
use YourVendor\YourAddon\ServiceProvider;
class BasicSmokeTest extends TestCase
{
/** @test */
public function the_addon_can_be_instantiated()
{
$provider = new ServiceProvider(null);
$this->assertInstanceOf(ServiceProvider::class, $provider);
}
/** @test */
public function required_services_exist()
{
// Check that your main service classes exist
$this->assertTrue(class_exists(\YourVendor\YourAddon\Services\YourService::class));
$this->assertTrue(class_exists(\YourVendor\YourAddon\Http\Controllers\YourController::class));
}
}
This might seem trivial, but it catches a surprising number of issues early!
If your main project uses Laravel Sail, you'll need to run tests inside the container:
# Run all addon tests
./vendor/bin/sail exec laravel.test bash -c "cd addons/your-addon && /var/www/html/vendor/bin/phpunit"
# Run with detailed output
./vendor/bin/sail exec laravel.test bash -c "cd addons/your-addon && /var/www/html/vendor/bin/phpunit --testdox"
# Run a specific test file
./vendor/bin/sail exec laravel.test bash -c "cd addons/your-addon && /var/www/html/vendor/bin/phpunit tests/Unit/YourTest.php"
Pro tip: Add these as scripts in your addon's composer.json:
{
"scripts": {
"test": "cd ../../ && ./vendor/bin/sail exec laravel.test bash -c 'cd addons/your-addon && /var/www/html/vendor/bin/phpunit'",
"test:unit": "cd ../../ && ./vendor/bin/sail exec laravel.test bash -c 'cd addons/your-addon && /var/www/html/vendor/bin/phpunit --testsuite=Unit'"
}
}
If your addon manipulates Bard fields, entries, or collections, focus on testing the transformation logic:
/** @test */
public function it_transforms_content_correctly()
{
// Create a simple data structure that mimics Statamic's format
$inputData = [
'title' => 'My Article',
'content' => 'Original content'
];
// Test your transformation logic
$transformer = new ContentTransformer();
$result = $transformer->transform($inputData);
// Assert the transformation worked
$this->assertEquals('Expected content', $result['content']);
}
Key insight: Test your business logic separately from Statamic's persistence layer.
For custom tags and fieldtypes, test the rendering logic:
/** @test */
public function custom_tag_renders_correctly()
{
$tag = new MyCustomTag();
$tag->setParameters(['limit' => 5]);
$tag->setContent('some content');
$output = $tag->index();
$this->assertStringContainsString('expected output', $output);
}
Test your controllers by mocking the dependencies:
use Mockery;
use Illuminate\Http\Request;
/** @test */
public function api_endpoint_returns_correct_data()
{
$request = Mockery::mock(Request::class);
$request->shouldReceive('input')
->with('search')
->andReturn('test query');
$controller = new ApiController();
$response = $controller->search($request);
$this->assertEquals(200, $response->getStatusCode());
$this->assertArrayHasKey('results', $response->getData(true));
}
Many Statamic features use facades that expect a full application. Here's how to handle them:
use Illuminate\Support\Facades\Config;
/** @test */
public function it_uses_configuration_correctly()
{
// Mock the Config facade
Config::shouldReceive('get')
->with('my-addon.settings.feature')
->once()
->andReturn('enabled');
$service = new MyService();
$this->assertTrue($service->isFeatureEnabled());
}
protected function tearDown(): void
{
// Clean up facade mocks
Config::clearResolvedInstances();
Mockery::close();
parent::tearDown();
}
When you can't avoid Statamic dependencies, create abstraction layers:
// Instead of using Entry::find() directly in your service...
class EntryRepository
{
public function find($id)
{
return Entry::find($id);
}
}
// ...you can now mock the repository in tests
$repository = Mockery::mock(EntryRepository::class);
$repository->shouldReceive('find')
->with('entry-123')
->andReturn($mockEntry);
Unit/: Test individual classes and methodsFeature/: Test complete features with multiple componentsIntegration/: Test interaction with Statamic (requires full installation)Build a base test class with common functionality:
<?php
namespace YourVendor\YourAddon\Tests;
use PHPUnit\Framework\TestCase as BaseTestCase;
use Mockery;
abstract class TestCase extends BaseTestCase
{
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
protected function createMockEntry($data = [])
{
$entry = Mockery::mock('Statamic\Entries\Entry');
foreach ($data as $key => $value) {
$entry->shouldReceive('get')
->with($key)
->andReturn($value);
}
return $entry;
}
}
Focus your testing efforts on:
// Good
public function it_throws_exception_when_configuration_is_missing()
public function it_returns_empty_array_when_no_results_found()
// Less helpful
public function testSearch()
public function test1()
Solution: Check your bootstrap file and ensure the namespace in your test matches your PSR-4 configuration.
Solution: Either mock the facade or run tests in a full Laravel environment.
Solution: Use in-memory SQLite for tests or mock your database queries:
Schema::create('test_entries', function ($table) {
$table->id();
$table->string('title');
$table->timestamps();
});
Testing Statamic addons doesn't have to be overwhelming. Start with:
Remember, the goal isn't 100% coverage. Focus on testing the parts that matter most to your users.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com