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 |