Module Testing
Each module in Auto Commerce can have its own test suite. This guide explains how to set up and write tests for your modules.
Overview
Module tests live within each module's backend/tests/ directory:
modules/YourModule/
├── backend/
│ └── tests/
│ ├── Feature/ # API and integration tests
│ │ └── YourApiTest.php
│ ├── Unit/ # Unit tests
│ │ └── YourModelTest.php
│ ├── Helpers.php # Test helper functions
│ └── Pest.php # Module test configuration
└── module.json
Setting Up Module Tests
1. Create Test Directories
mkdir -p modules/YourModule/backend/tests/Feature
mkdir -p modules/YourModule/backend/tests/Unit
2. Create Pest.php Configuration
Create modules/YourModule/backend/tests/Pest.php:
<?php
/**
* YourModule Test Configuration
*/
use Illuminate\Foundation\Testing\DatabaseTransactions;
// Load helper functions
require_once __DIR__ . '/Helpers.php';
// Apply base test case and database transactions to all tests
uses(Tests\TestCase::class, DatabaseTransactions::class)->in('Feature', 'Unit');
// Custom expectations for your module
expect()->extend('toBeActiveYourThing', function () {
return $this->toBeInstanceOf(YourModel::class)
->and($this->value->is_active)->toBeTrue();
});
3. Create Helper Functions
Create modules/YourModule/backend/tests/Helpers.php:
<?php
/**
* YourModule Test Helpers
*/
use Modules\YourModule\App\Models\YourModel;
if (!function_exists('createTestYourModel')) {
/**
* Create a test model with default attributes.
*/
function createTestYourModel(array $attributes = []): YourModel
{
return YourModel::create(array_merge([
'name' => 'Test Item',
'status' => 'active',
// ... default attributes
], $attributes));
}
}
if (!function_exists('createMultipleTestYourModels')) {
/**
* Create multiple test models.
*/
function createMultipleTestYourModels(int $count, array $attributes = []): \Illuminate\Support\Collection
{
return collect(range(1, $count))->map(function ($i) use ($attributes) {
return createTestYourModel(array_merge([
'name' => "Test Item {$i}",
], $attributes));
});
}
}
Writing Feature Tests
Feature tests verify your module's API endpoints work correctly.
Example: API Test
Create modules/YourModule/backend/tests/Feature/YourApiTest.php:
<?php
/**
* YourModule API Tests
*/
require_once __DIR__ . '/../Helpers.php';
use App\Models\User;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Laravel\Passport\Passport;
uses(Tests\TestCase::class, DatabaseTransactions::class);
describe('YourModule API', function () {
beforeEach(function () {
// Create tenant and user for tests
$this->tenant = Tenant::create([
'id' => 'test-' . uniqid(),
'name' => 'Test Tenant',
]);
tenancy()->initialize($this->tenant);
$this->user = User::create([
'name' => 'Test User',
'email' => 'test-' . uniqid() . '@example.com',
'password' => bcrypt('password'),
]);
tenancy()->end();
});
afterEach(function () {
tenancy()->end();
$this->tenant->delete();
});
describe('Index Endpoint', function () {
it('returns list of items', function () {
Passport::actingAs($this->user);
$response = $this->withHeaders(['X-Tenant-Id' => $this->tenant->id])
->getJson('/api/v1/your-module/items');
$response->assertStatus(200)
->assertJsonStructure(['data']);
});
it('requires authentication', function () {
$response = $this->getJson('/api/v1/your-module/items');
$response->assertStatus(401);
});
});
describe('Create Endpoint', function () {
it('can create item with valid data', function () {
Passport::actingAs($this->user);
$response = $this->withHeaders(['X-Tenant-Id' => $this->tenant->id])
->postJson('/api/v1/your-module/items', [
'name' => 'New Item',
'description' => 'Test description',
]);
$response->assertStatus(201)
->assertJsonPath('data.name', 'New Item');
});
it('validates required fields', function () {
Passport::actingAs($this->user);
$response = $this->withHeaders(['X-Tenant-Id' => $this->tenant->id])
->postJson('/api/v1/your-module/items', []);
$response->assertStatus(422)
->assertJsonValidationErrors(['name']);
});
});
});
Writing Unit Tests
Unit tests verify individual components work correctly in isolation.
Example: Model Test
Create modules/YourModule/backend/tests/Unit/YourModelTest.php:
<?php
/**
* YourModel Unit Tests
*/
require_once __DIR__ . '/../Helpers.php';
use Modules\YourModule\App\Models\YourModel;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(Tests\TestCase::class, DatabaseTransactions::class);
describe('YourModel', function () {
describe('Status Constants', function () {
it('has active status constant', function () {
expect(YourModel::STATUS_ACTIVE)->toBe('active');
});
it('has inactive status constant', function () {
expect(YourModel::STATUS_INACTIVE)->toBe('inactive');
});
});
describe('Scopes', function () {
it('filters active items', function () {
createTestYourModel(['status' => YourModel::STATUS_ACTIVE]);
createTestYourModel(['status' => YourModel::STATUS_ACTIVE]);
createTestYourModel(['status' => YourModel::STATUS_INACTIVE]);
$activeItems = YourModel::active()->count();
expect($activeItems)->toBe(2);
});
});
describe('Accessors', function () {
it('calculates is_active correctly', function () {
$activeItem = createTestYourModel(['status' => YourModel::STATUS_ACTIVE]);
$inactiveItem = createTestYourModel(['status' => YourModel::STATUS_INACTIVE]);
expect($activeItem->is_active)->toBeTrue();
expect($inactiveItem->is_active)->toBeFalse();
});
});
describe('Methods', function () {
it('can activate item', function () {
$item = createTestYourModel(['status' => YourModel::STATUS_INACTIVE]);
$item->activate();
expect($item->fresh()->status)->toBe(YourModel::STATUS_ACTIVE);
});
it('can deactivate item', function () {
$item = createTestYourModel(['status' => YourModel::STATUS_ACTIVE]);
$item->deactivate();
expect($item->fresh()->status)->toBe(YourModel::STATUS_INACTIVE);
});
});
});
Running Module Tests
Using the test:module Command
Auto Commerce provides an Artisan command to run module tests:
# List all modules with tests
php artisan test:module --list
# Run tests for a specific module
php artisan test:module Products
# Run only unit tests for a module
php artisan test:module Products --unit
# Run only feature tests for a module
php artisan test:module Products --feature
# Filter tests by name
php artisan test:module Products --filter="can create"
# Generate coverage report
php artisan test:module Products --coverage
Using Pest Directly
# Run all module tests
./vendor/bin/pest ../modules/Products/backend/tests
# Run specific test type
./vendor/bin/pest ../modules/Products/backend/tests/Feature
# Run with coverage
./vendor/bin/pest ../modules/Products/backend/tests --coverage
Test Configuration in phpunit.xml
Module tests are included in the main test suite configuration:
<testsuites>
<!-- ... other suites ... -->
<!-- All module tests -->
<testsuite name="Modules">
<directory>../modules/*/backend/tests</directory>
</testsuite>
<!-- Specific module test suite -->
<testsuite name="Module-Products">
<directory>../modules/Products/backend/tests</directory>
</testsuite>
</testsuites>
Testing with Tenant Context
Many module features require tenant context. Here's how to handle it:
For Models in Tenant Database
use App\Models\Tenant;
use Illuminate\Support\Facades\Schema;
beforeEach(function () {
$this->tenant = Tenant::create([
'id' => 'test-tenant-' . uniqid(),
'name' => 'Test Tenant',
]);
// Initialize tenant and check if required tables exist
tenancy()->initialize($this->tenant);
if (!Schema::hasTable('your_table')) {
$this->markTestSkipped('Tenant database not set up.');
}
});
afterEach(function () {
tenancy()->end();
$this->tenant->delete();
});
For API Tests with Tenant Context
it('can access tenant data', function () {
Passport::actingAs($this->user);
$response = $this->withHeaders([
'X-Tenant-Id' => $this->tenant->id,
])->getJson('/api/v1/your-module/data');
$response->assertStatus(200);
});
Best Practices for Module Tests
1. Keep Tests Independent
Each test should be self-contained and not depend on other tests:
// Good - Creates its own data
it('can update item', function () {
$item = createTestYourModel(['name' => 'Original']);
$item->update(['name' => 'Updated']);
expect($item->fresh()->name)->toBe('Updated');
});
// Bad - Depends on external state
it('can update item', function () {
$item = YourModel::first(); // Might not exist!
$item->update(['name' => 'Updated']);
});
2. Use Descriptive Test Names
// Good
it('returns 404 when item not found', function () { ... });
it('validates email format on registration', function () { ... });
it('prevents duplicate SKU creation', function () { ... });
// Bad
it('test1', function () { ... });
it('works', function () { ... });
3. Test Edge Cases
describe('Stock Management', function () {
it('allows stock adjustment when in stock');
it('prevents negative stock when inventory tracked');
it('allows any quantity when inventory not tracked');
it('handles zero quantity correctly');
it('handles very large quantities');
});
4. Group Related Tests
describe('Product API', function () {
describe('List Products', function () {
it('returns paginated results');
it('filters by category');
it('sorts by name');
});
describe('Create Product', function () {
it('creates with valid data');
it('validates required fields');
it('prevents duplicate SKU');
});
});
5. Clean Up Test Data
Use DatabaseTransactions trait or manual cleanup:
uses(DatabaseTransactions::class); // Automatic rollback
// Or manual cleanup
afterEach(function () {
YourModel::truncate();
});
Module Test Checklist
When creating tests for a new module, ensure you cover:
- Model creation and validation
- Model scopes and queries
- Model accessors and mutators
- Model relationships
- API endpoints (CRUD operations)
- API authentication and authorization
- API validation errors
- Tenant context (if applicable)
- Edge cases and error handling
- Business logic methods
Related Documentation
- Testing Overview - General testing guide
- Module Structure - Module directory structure
- Creating Modules - Getting started with modules