Skip to content

Testing Strategy

Status: Final Version: 1.0


Purpose

Define test types, coverage targets, fixture strategy, and CI/CD integration for the ESG platform.


Test Pyramid

Layer Ratio Tools Coverage Target
Unit Tests 70% PHPUnit 80% code coverage
Feature Tests 20% PHPUnit + Laravel HTTP tests Critical workflows
Integration Tests 8% PHPUnit + real DB API contracts, queue jobs
E2E Tests 2% Laravel Dusk (Selenium) Critical user journeys

Unit Tests

Scope: Model methods, service classes, validation logic

Example: Validation Rule Test

class MetricValidationServiceTest extends TestCase
{
    public function test_numeric_validation_passes_for_valid_number()
    {
        $rule = ['type' => 'schema', 'rule' => 'numeric'];
        $submission = MetricSubmission::factory()->make(['raw_data' => ['value' => 123.45]]);

        $validator = new ValidationService();
        $result = $validator->validateSchema($submission, (object) $rule);

        $this->assertTrue($result);
    }

    public function test_numeric_validation_fails_for_string()
    {
        $rule = ['type' => 'schema', 'rule' => 'numeric'];
        $submission = MetricSubmission::factory()->make(['raw_data' => ['value' => 'abc']]);

        $validator = new ValidationService();
        $result = $validator->validateSchema($submission, (object) $rule);

        $this->assertFalse($result);
    }
}


Feature Tests

Scope: API endpoints, RBAC policies, workflows

Example: Submission Approval Test

class SubmissionApprovalTest extends TestCase
{
    use RefreshDatabase;

    public function test_approver_can_approve_submission()
    {
        $approver = User::factory()->role('approver')->create();
        $submission = MetricSubmission::factory()->state('VALIDATED')->create();

        $this->actingAs($approver)
            ->postJson("/api/v1/admin/submissions/{$submission->id}/approve", [
                'comment' => 'Data verified',
            ])
            ->assertStatus(200)
            ->assertJson(['state' => 'APPROVED']);

        $this->assertDatabaseHas('metric_submissions', [
            'id' => $submission->id,
            'state' => 'APPROVED',
            'approved_by_user_id' => $approver->id,
        ]);
    }

    public function test_collector_cannot_approve_submission()
    {
        $collector = User::factory()->role('collector')->create();
        $submission = MetricSubmission::factory()->state('VALIDATED')->create();

        $this->actingAs($collector)
            ->postJson("/api/v1/admin/submissions/{$submission->id}/approve")
            ->assertStatus(403); // Forbidden
    }
}


Audit Log Invariant Tests

Requirement: Every state transition MUST create audit log entry.

public function test_submission_approval_creates_audit_log()
{
    $approver = User::factory()->role('approver')->create();
    $submission = MetricSubmission::factory()->state('VALIDATED')->create();

    $this->actingAs($approver)
        ->postJson("/api/v1/admin/submissions/{$submission->id}/approve");

    $this->assertDatabaseHas('audit_logs', [
        'action' => 'submission.approved',
        'entity_type' => 'MetricSubmission',
        'entity_id' => $submission->id,
        'actor_user_id' => $approver->id,
    ]);
}

API Contract Tests

Scope: Validate request/response schemas match documentation.

Example: Submission Create Contract

public function test_submission_create_response_matches_schema()
{
    $collector = User::factory()->role('collector')->create();

    $response = $this->actingAs($collector)
        ->postJson('/api/v1/collector/submissions', [
            'submission_uuid' => Str::uuid(),
            'reporting_period_id' => 1,
            'site_id' => 1,
            'metric_id' => 'GRI_302_1_ELECTRICITY',
            'value' => 1250.50,
            'unit' => 'MWh',
        ])
        ->assertStatus(201);

    // Assert response structure
    $response->assertJsonStructure([
        'id',
        'submission_uuid',
        'state',
        'submitted_at',
        'validation_status',
        'next_steps',
    ]);
}


Data Fixtures & Seeders

Factories

// database/factories/MetricSubmissionFactory.php
class MetricSubmissionFactory extends Factory
{
    public function definition()
    {
        return [
            'tenant_id' => Tenant::factory(),
            'submission_uuid' => Str::uuid(),
            'reporting_period_id' => ReportingPeriod::factory(),
            'site_id' => Site::factory(),
            'metric_definition_id' => MetricDefinition::factory(),
            'raw_data' => ['value' => $this->faker->randomFloat(2, 0, 10000)],
            'state' => 'RECEIVED',
            'submitted_by_user_id' => User::factory(),
            'submitted_at' => now(),
        ];
    }

    public function validated()
    {
        return $this->state(['state' => 'VALIDATED']);
    }

    public function approved()
    {
        return $this->state([
            'state' => 'APPROVED',
            'approved_by_user_id' => User::factory()->role('approver'),
            'approved_at' => now(),
        ]);
    }
}

Seeders

// database/seeders/DemoDataSeeder.php
class DemoDataSeeder extends Seeder
{
    public function run()
    {
        $tenant = Tenant::create(['name' => 'Demo Corp', 'slug' => 'demo-corp']);

        $org = Organisation::create([
            'tenant_id' => $tenant->id,
            'name' => 'Demo Corporation',
            'code' => 'DEMO',
        ]);

        $period = ReportingPeriod::create([
            'tenant_id' => $tenant->id,
            'organisation_id' => $org->id,
            'name' => 'FY2025',
            'start_date' => '2025-01-01',
            'end_date' => '2025-12-31',
            'fiscal_year' => 2025,
            'state' => 'OPEN',
        ]);

        // Create 100 sample submissions
        MetricSubmission::factory(100)
            ->for($period)
            ->create(['tenant_id' => $tenant->id]);
    }
}

CI/CD Pipeline

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: esg_test
          POSTGRES_PASSWORD: password
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
    steps:
      - uses: actions/checkout@v3
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.2
          extensions: pdo, pgsql
      - name: Install Dependencies
        run: composer install
      - name: Run Migrations
        run: php artisan migrate --env=testing
      - name: Run Tests
        run: vendor/bin/phpunit --coverage-clover coverage.xml
      - name: Upload Coverage
        uses: codecov/codecov-action@v3

Acceptance Criteria

  • Unit test coverage ≥ 80% (measured by PHPUnit --coverage)
  • All API endpoints have feature tests (request/response validation)
  • Audit log invariant tests pass (every mutation creates log)
  • Factories exist for all core entities (submissions, evidence, users)
  • Demo data seeder creates realistic test data
  • CI/CD pipeline runs tests on every PR
  • Code coverage report uploaded to Codecov

Cross-References


Change Log

Version Date Author Changes
1.0 2026-01-03 Senior Product Architect Initial testing strategy specification