# Testing Guidelines & Standards

## Overview

This document outlines the testing standards and best practices for the CBTAPPS project. All developers must follow these guidelines to ensure consistent, maintainable, and effective tests.

## Testing Philosophy

- **Tests are code**: Treat tests with the same care as production code
- **Fast feedback**: Tests should run quickly to enable rapid development
- **Isolation**: Each test should be independent and not rely on other tests
- **Clarity**: Test names should clearly describe what they test
- **Maintainability**: Tests should be easy to understand and modify

## Test Structure

### Naming Convention

Use the format: `test_[feature]_[expected_result]()`

```php
// ✅ Good
public function test_student_can_take_exam(): void
public function test_exam_requires_authentication(): void
public function test_token_balance_deducts_on_exam_start(): void

// ❌ Bad
public function testExam(): void
public function test1(): void
public function testStudentExam(): void
```

### Given-When-Then Pattern

Organize tests using the Given-When-Then pattern:

```php
public function test_student_can_submit_exam_attempt(): void
{
    // Given - Setup test data and preconditions
    $student = Student::factory()->active()->create();
    $exam = Exam::factory()
        ->active()
        ->has(Question::factory()->count(10))
        ->create();

    // When - Perform the action being tested
    $attempt = ExamAttempt::factory()->create([
        'exam_id' => $exam->id,
        'student_id' => $student->id,
        'status' => 'in_progress',
    ]);

    $attempt->submit();

    // Then - Assert the expected outcome
    $this->assertEquals('submitted', $attempt->status);
    $this->assertNotNull($attempt->end_time);
}
```

## Test Organization

### Directory Structure

```
tests/
├── Unit/              # Unit tests for individual components
│   ├── Models/       # Model tests
│   ├── Services/     # Service layer tests
│   └── Helpers/      # Helper function tests
├── Feature/          # Feature/API tests
│   ├── Api/         # API endpoint tests
│   ├── Web/         # Web route tests
│   └── Console/     # Console command tests
└── Integration/      # End-to-end workflow tests
```

### Test Categories

1. **Unit Tests**: Test individual classes/methods in isolation
2. **Feature Tests**: Test HTTP endpoints and user interactions
3. **Integration Tests**: Test complete workflows across multiple components

## Testing Best Practices

### 1. Use Factories for Test Data

```php
// ✅ Good - Uses factories
public function test_exam_has_many_questions(): void
{
    $exam = Exam::factory()
        ->has(Question::factory()->count(5))
        ->create();

    $this->assertCount(5, $exam->questions);
}

// ❌ Bad - Manual data creation
public function test_exam_has_many_questions(): void
{
    $exam = Exam::create([
        'name' => 'Test Exam',
        'lbb_id' => 1,
        // ... lots of fields
    ]);

    Question::create([
        'exam_id' => $exam->id,
        'question_text' => 'Test?',
        // ... lots of fields
    ]);

    // ... repetitive code
}
```

### 2. Use Descriptive Assertions

```php
// ✅ Good - Descriptive
$this->assertEquals('completed', $attempt->status, 'Exam attempt should be marked as completed');
$this->assertDatabaseHas('exam_attempts', [
    'id' => $attempt->id,
    'status' => 'completed',
]);

// ❌ Bad - Vague
$this->assertTrue($attempt->status === 'completed');
```

### 3. Test One Thing Per Test

```php
// ✅ Good - Single responsibility
public function test_exam_score_calculation(): void
{
    $attempt = ExamAttempt::factory()->create([
        'correct_answers' => 8,
        'total_questions' => 10,
    ]);

    $this->assertEquals(80, $attempt->score);
}

public function test_exam_passing_status(): void
{
    $attempt = ExamAttempt::factory()->create([
        'score' => 75,
    ]);

    $this->assertTrue($attempt->is_passed);
}

// ❌ Bad - Testing multiple things
public function test_exam_attempt(): void
{
    $attempt = ExamAttempt::factory()->create();

    $this->assertEquals(80, $attempt->score);
    $this->assertTrue($attempt->is_passed);
    $this->assertEquals('completed', $attempt->status);
    // ... too many assertions
}
```

### 4. Use RefreshDatabase Trait

Always use `RefreshDatabase` trait for tests that interact with the database:

```php
use Illuminate\Foundation\Testing\RefreshDatabase;

class ExamTest extends TestCase
{
    use RefreshDatabase; // Resets database after each test

    public function test_exam_creation(): void
    {
        // Database is fresh for each test
        $exam = Exam::factory()->create();
        $this->assertDatabaseHas('exams', ['id' => $exam->id]);
    }
}
```

### 5. Mock External Dependencies

Mock external services and APIs:

```php
public function test_file_upload_uses_storage_service(): void
{
    Storage::fake('gcs');

    $file = UploadedFile::fake()->create('document.pdf', 1000);

    $result = $this->fileService->upload($file);

    Storage::disk('gcs')->assertExists($result->path);
}
```

## Coverage Requirements

### Target Coverage by Component Type

- **Services**: 85% coverage (critical business logic)
- **Models**: 75% coverage (data layer)
- **Controllers**: 60% coverage (HTTP layer)
- **Helpers**: 80% coverage (utilities)
- **Overall**: 70% coverage

### What to Test

#### Services (Priority: HIGH)
- Business logic validation
- Error handling
- Edge cases
- Integration with external services
- Transaction handling

#### Models (Priority: HIGH)
- Relationships
- Scopes
- Accessors/Mutators
- Model events
- Validation rules

#### Controllers (Priority: MEDIUM)
- HTTP status codes
- Response formats
- Authentication/Authorization
- Request validation
- Error responses

#### Helpers (Priority: MEDIUM)
- Input validation
- Output formatting
- Edge cases
- Error handling

## Running Tests

### Run All Tests

```bash
composer test
```

### Run Specific Test Suite

```bash
# Unit tests only
php artisan test --testsuite=Unit

# Feature tests only
php artisan test --testsuite=Feature

# Specific test file
php artisan test --filter=ExamServiceTest
```

### Run with Coverage

```bash
php artisan test --coverage
```

## Common Test Patterns

### Authentication Test

```php
public function test_exam_requires_authentication(): void
{
    $response = $this->get(route('exams.show', 1));

    $response->assertRedirect(route('login'));
}

public function test_authenticated_user_can_access_exam(): void
{
    $user = User::factory()->create();
    $exam = Exam::factory()->create();

    $response = $this->actingAs($user)
        ->get(route('exams.show', $exam));

    $response->assertStatus(200);
}
```

### Authorization Test

```php
public function test_student_cannot_access_other_students_attempts(): void
{
    $student1 = Student::factory()->create();
    $student2 = Student::factory()->create();
    $attempt = ExamAttempt::factory()->create(['student_id' => $student2->id]);

    $response = $this->actingAs($student1->user)
        ->get(route('attempts.show', $attempt));

    $response->assertForbidden();
}
```

### Validation Test

```php
public function test_exam_creation_requires_valid_data(): void
{
    $admin = User::factory()->admin()->create();

    $response = $this->actingAs($admin)
        ->post(route('exams.store'), [
            'name' => '', // Invalid: empty name
            'duration' => -10, // Invalid: negative duration
        ]);

    $response->assertSessionHasErrors(['name', 'duration']);
}
```

### API Response Test

```php
public function test_api_returns_exam_list(): void
{
    Exam::factory()->count(3)->create();

    $response = $this->getJson(route('api.exams.index'));

    $response->assertStatus(200)
        ->assertJsonCount(3, 'data')
        ->assertJsonStructure([
            'data' => [
                '*' => ['id', 'name', 'exam_code', 'start_date']
            ]
        ]);
}
```

## CI/CD Integration

### GitHub Actions Workflow

Tests run automatically on:
- Every push to main/develop branches
- Every pull request
- Before deployment

### Test Requirements

- All tests must pass before merge
- No skipped tests in PRs
- Coverage must not decrease

## Debugging Tests

### Run Single Test with Output

```bash
php artisan test --filter test_exam_creation --dump
```

### Use dd() in Tests

```php
public function test_exam_creation(): void
{
    $exam = Exam::factory()->create();

    dd($exam->toArray()); // Debug output
}
```

### See Detailed Errors

```bash
php artisan test --verbose
```

## Code Review Checklist

When reviewing tests, check for:
- [ ] Test names are descriptive
- [ ] Tests use factories, not manual data creation
- [ ] Each test tests one thing
- [ ] Tests are independent (no test dependencies)
- [ ] Proper use of assertions
- [ ] External dependencies are mocked
- [ ] Database is properly isolated (RefreshDatabase)
- [ ] Tests cover edge cases
- [ ] Tests are fast (< 1 second per test)

## Resources

- [Laravel Testing Documentation](https://laravel.com/docs/testing)
- [PHPUnit Documentation](https://phpunit.de/documentation.html)
- [Testing Best Practices](https://testingjavascript.com/)

## Questions?

If you have questions about testing standards or need guidance, please contact the Senior QA or create a discussion in the repository.
