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