Files
radicall/docs/TESTING.md
T

205 lines
14 KiB
Markdown

# Testing Guide
Tests are written **only on explicit request**. When asked, follow this document exactly and keep the
coverage matrix at the bottom up to date so remaining gaps are always visible.
## 1. What we test (and what we don't)
Business logic lives in `app/Services/`. Models are mostly relations and `$fillable`. Testing is
prioritised accordingly:
| Priority | Layer | Why |
|----------|-------|-----|
| P1 | Services | Money, auth, scheduling, case flow — the only place real logic lives |
| P1 | Form Requests | Cheap, catches every invalid input path before it reaches a service |
| P1 | HTTP (critical routes) | Authentication, authorization/ownership, payment callback |
| P2 | Remaining HTTP | Doctor & Admin CRUD |
| P3 | Enums | Only methods with logic (`price()`, `processingHours()`, `fromDate()`, …) |
| P3 | Models | Only scopes, accessors and helper methods — never relations or `$fillable` |
**Never test:** Eloquent relations, `$fillable`/`$casts`, framework behaviour, API Resources on their
own (they are asserted through HTTP tests), Vue components (no JS test stack in this project).
## 2. Directory structure — one source file ↔ one test file
Test files mirror the source path. This is what makes gap detection mechanical.
```
app/Services/Wallet/WalletService.php → tests/Feature/Services/Wallet/WalletServiceTest.php
app/Http/Requests/Client/PaymentRequest.php → tests/Feature/Requests/Client/PaymentRequestTest.php
app/Http/Controllers/Client/PaymentController.php → tests/Feature/Http/Client/PaymentControllerTest.php
app/Enums/ShiftDayEnum.php → tests/Unit/Enums/ShiftDayEnumTest.php
app/Models/Shift.php → tests/Feature/Models/ShiftTest.php
```
- `tests/Unit/` — no database, no facades. Enums only.
- `tests/Feature/` — boots the app, uses `RefreshDatabase` (sqlite `:memory:`, already configured in `phpunit.xml`).
- The default `ExampleTest` placeholders have been removed; never re-add them.
## 3. Conventions
- PHPUnit 11, class-based tests extending `Tests\TestCase`, `RefreshDatabase` trait on every Feature test.
- Method names: `test_{action}_{expected}_when_{condition}` in snake_case, e.g.
`test_deposit_increases_balance`, `test_withdraw_throws_when_balance_is_insufficient`.
- One behaviour per test. Arrange / Act / Assert, separated by a blank line, no comments.
- Resolve services from the container (`app(WalletService::class)`), never `new`.
- Every model touched by a test must have a factory in `database/factories/`. Create the missing factory
first, in the same style as the existing ones. Factories must produce valid rows with all foreign keys.
- Assert outcomes, not implementation: `assertDatabaseHas`, returned values, thrown exceptions,
`assertInertia` props / JSON structure for HTTP.
- Use enums in assertions (`DocumentStatusEnum::Pending`), never raw strings.
### Fakes — never hit a real external system
| Dependency | Fake |
|------------|------|
| Zibal (`ZibalPaymentService`), IranPayamak (`IranPayamakService`) | `Http::fake([...])` with `Http::assertSent` |
| `SendPatternSms` job / `SmsNotificationService` | `Queue::fake()` + `Queue::assertPushed(SendPatternSms::class)` |
| `FileStorageService`, `Medium` | `Storage::fake('public')`, `Storage::fake('private')` |
| Time-dependent logic (shifts, OTP expiry, processing hours) | `Carbon::setTestNow(...)` |
| Authenticated user | `$this->actingAs(User::factory()->create())` |
| Spatie roles/permissions | seed `PermissionSeeder` in the test, then `$user->givePermissionTo(...)` / `assignRole(...)` |
## 4. Required coverage per layer
**Service** — happy path; every guard / exception branch; every side effect (DB rows, dispatched
jobs, transactions written); for money code, balance invariants before and after.
**Form Request** — `authorize()` when it is not `return true`; each rule with an invalid input →
`assertSessionHasErrors('field')` / 422; one fully valid payload passes.
**HTTP** — guest is redirected to login; wrong role or non-owner gets 403/404; happy path returns
the expected status and persisted state; Inertia page tests assert the component name and key props.
**Enum** — every method containing logic, including edge values.
**Model** — each scope filters correctly; each accessor / helper method (`isExpired`, `fullUrl`, `humanSize`, …).
## 5. Workflow when asked to write tests
1. Read this file. If the user names a target, test that; otherwise take the next `☐` row by priority (P1 → P3).
2. Create missing factories first.
3. Write the test file at the mirrored path (section 2).
4. Run `php artisan test --filter={ClassName}` and make it green. Then run the full suite.
5. Flip the row to `☑` in the matrix below. Add rows for any new Service / Request / Controller / Enum that exists in the code but is missing from the matrix.
6. Do not modify unrelated tests or source files.
### Detecting gaps mechanically
The matrix is the source of truth, but this catches rows that were never added. Anything it lists
that is not in the matrix must be added as `☐` or, if it has no logic worth testing, as `—`:
```bash
for f in $(find app/Services app/Http/Requests app/Http/Controllers app/Enums -name '*.php' ! -name 'Controller.php' ! -name 'EnumHasOptions.php'); do t=$(echo "$f" | sed -E 's#^app/Services/#tests/Feature/Services/#; s#^app/Http/Requests/#tests/Feature/Requests/#; s#^app/Http/Controllers/#tests/Feature/Http/#; s#^app/Enums/#tests/Unit/Enums/#; s#\.php$#Test.php#'); [ -f "$t" ] || echo "MISSING $t"; done
```
## 6. Coverage matrix
Legend: `☐` not written · `☑` written · `—` intentionally not tested
### Services
| Priority | Source | Test | Status |
|----------|--------|------|--------|
| P1 | `app/Services/Wallet/WalletService.php` | `tests/Feature/Services/Wallet/WalletServiceTest.php` | ☑ |
| P1 | `app/Services/Wallet/WithdrawRequestService.php` | `tests/Feature/Services/Wallet/WithdrawRequestServiceTest.php` | ☑ |
| P1 | `app/Services/Invoice/InvoiceService.php` | `tests/Feature/Services/Invoice/InvoiceServiceTest.php` | ☑ |
| P1 | `app/Services/Payment/ZibalPaymentService.php` | `tests/Feature/Services/Payment/ZibalPaymentServiceTest.php` | ☑ |
| P1 | `app/Services/Auth/OtpService.php` | `tests/Feature/Services/Auth/OtpServiceTest.php` | ☐ |
| P1 | `app/Services/Auth/AuthService.php` | `tests/Feature/Services/Auth/AuthServiceTest.php` | ☐ |
| P1 | `app/Services/Auth/RegistrationService.php` | `tests/Feature/Services/Auth/RegistrationServiceTest.php` | ☐ |
| P1 | `app/Services/Shift/ShiftScheduleService.php` | `tests/Feature/Services/Shift/ShiftScheduleServiceTest.php` | ☐ |
| P1 | `app/Services/Document/CaseService.php` | `tests/Feature/Services/Document/CaseServiceTest.php` | ☐ |
| P1 | `app/Services/Document/FastRequestService.php` | `tests/Feature/Services/Document/FastRequestServiceTest.php` | ☐ |
| P2 | `app/Services/Document/DocumentCodeService.php` | `tests/Feature/Services/Document/DocumentCodeServiceTest.php` | ☐ |
| P2 | `app/Services/Sms/SmsNotificationService.php` | `tests/Feature/Services/Sms/SmsNotificationServiceTest.php` | ☐ |
| P2 | `app/Services/FileStorageService.php` | `tests/Feature/Services/FileStorageServiceTest.php` | ☐ |
| P3 | `app/Services/Sms/IranPayamakService.php` | `tests/Feature/Services/Sms/IranPayamakServiceTest.php` | ☐ |
### Form Requests
| Priority | Source | Test | Status |
|----------|--------|------|--------|
| P1 | `app/Http/Requests/Client/Auth/UserRegistrationRequest.php` | `tests/Feature/Requests/Client/Auth/UserRegistrationRequestTest.php` | ☐ |
| P1 | `app/Http/Requests/Client/Auth/VerifyOtpRequest.php` | `tests/Feature/Requests/Client/Auth/VerifyOtpRequestTest.php` | ☐ |
| P1 | `app/Http/Requests/Client/PaymentRequest.php` | `tests/Feature/Requests/Client/PaymentRequestTest.php` | ☐ |
| P1 | `app/Http/Requests/Client/WalletDepositRequest.php` | `tests/Feature/Requests/Client/WalletDepositRequestTest.php` | ☐ |
| P1 | `app/Http/Requests/Client/WithdrawRequestStoreRequest.php` | `tests/Feature/Requests/Client/WithdrawRequestStoreRequestTest.php` | ☐ |
| P1 | `app/Http/Requests/Client/BankCardStoreRequest.php` | `tests/Feature/Requests/Client/BankCardStoreRequestTest.php` | ☐ |
| P1 | `app/Http/Requests/Client/FastRequestRequest.php` | `tests/Feature/Requests/Client/FastRequestRequestTest.php` | ☐ |
| P2 | `app/Http/Requests/Client/CaseRejectRequest.php` | `tests/Feature/Requests/Client/CaseRejectRequestTest.php` | ☐ |
| P2 | `app/Http/Requests/Client/ReportSubmitRequest.php` | `tests/Feature/Requests/Client/ReportSubmitRequestTest.php` | ☐ |
| P2 | `app/Http/Requests/Client/ProfileUpdateRequest.php` | `tests/Feature/Requests/Client/ProfileUpdateRequestTest.php` | ☐ |
| P2 | `app/Http/Requests/Admin/WithdrawRejectRequest.php` | `tests/Feature/Requests/Admin/WithdrawRejectRequestTest.php` | ☐ |
| P2 | `app/Http/Requests/Admin/DocumentRequest.php` | `tests/Feature/Requests/Admin/DocumentRequestTest.php` | ☐ |
| P2 | `app/Http/Requests/Admin/ShiftRequest.php` | `tests/Feature/Requests/Admin/ShiftRequestTest.php` | ☐ |
| P2 | `app/Http/Requests/Admin/UserRequest.php` | `tests/Feature/Requests/Admin/UserRequestTest.php` | ☐ |
| P2 | `app/Http/Requests/Admin/PetRequest.php` | `tests/Feature/Requests/Admin/PetRequestTest.php` | ☐ |
| P2 | `app/Http/Requests/Admin/ReportRequest.php` | `tests/Feature/Requests/Admin/ReportRequestTest.php` | ☐ |
| P2 | `app/Http/Requests/Admin/SettingProfileRequest.php` | `tests/Feature/Requests/Admin/SettingProfileRequestTest.php` | ☐ |
### HTTP
| Priority | Source | Test | Status |
|----------|--------|------|--------|
| P1 | `app/Http/Controllers/Client/AuthController.php` | `tests/Feature/Http/Client/AuthControllerTest.php` | ☐ |
| P1 | `app/Http/Controllers/Client/PaymentController.php` | `tests/Feature/Http/Client/PaymentControllerTest.php` | ☐ |
| P1 | `app/Http/Controllers/Client/WalletController.php` | `tests/Feature/Http/Client/WalletControllerTest.php` | ☐ |
| P1 | `app/Http/Controllers/Client/FastRequestController.php` | `tests/Feature/Http/Client/FastRequestControllerTest.php` | ☐ |
| P1 | `app/Http/Controllers/Client/InvoiceController.php` | `tests/Feature/Http/Client/InvoiceControllerTest.php` | ☐ |
| P1 | `app/Http/Controllers/Admin/WithdrawRequestController.php` | `tests/Feature/Http/Admin/WithdrawRequestControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Client/Doctor/CaseController.php` | `tests/Feature/Http/Client/Doctor/CaseControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Client/Doctor/ReportController.php` | `tests/Feature/Http/Client/Doctor/ReportControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Client/Doctor/ShiftController.php` | `tests/Feature/Http/Client/Doctor/ShiftControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Client/Doctor/DocumentController.php` | `tests/Feature/Http/Client/Doctor/DocumentControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Client/Doctor/DashboardController.php` | `tests/Feature/Http/Client/Doctor/DashboardControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Client/MediaController.php` | `tests/Feature/Http/Client/MediaControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Client/ProfileController.php` | `tests/Feature/Http/Client/ProfileControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Admin/AuthController.php` | `tests/Feature/Http/Admin/AuthControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Admin/DocumentController.php` | `tests/Feature/Http/Admin/DocumentControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Admin/ShiftController.php` | `tests/Feature/Http/Admin/ShiftControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Admin/UserController.php` | `tests/Feature/Http/Admin/UserControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Admin/ReportController.php` | `tests/Feature/Http/Admin/ReportControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Admin/PetController.php` | `tests/Feature/Http/Admin/PetControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Admin/MediumController.php` | `tests/Feature/Http/Admin/MediumControllerTest.php` | ☐ |
| P2 | `app/Http/Controllers/Admin/SettingUserController.php` | `tests/Feature/Http/Admin/SettingUserControllerTest.php` | ☐ |
| P3 | `app/Http/Controllers/Admin/Role/RoleController.php` | `tests/Feature/Http/Admin/Role/RoleControllerTest.php` | ☐ |
| P3 | `app/Http/Controllers/Admin/Role/PermissionController.php` | `tests/Feature/Http/Admin/Role/PermissionControllerTest.php` | ☐ |
| P3 | `app/Http/Controllers/Admin/PanelController.php` | `tests/Feature/Http/Admin/PanelControllerTest.php` | ☐ |
### Enums
| Priority | Source | Test | Status |
|----------|--------|------|--------|
| P3 | `app/Enums/ShiftDayEnum.php` | `tests/Unit/Enums/ShiftDayEnumTest.php` | ☐ |
| P3 | `app/Enums/DocumentModalityEnum.php` | `tests/Unit/Enums/DocumentModalityEnumTest.php` | ☐ |
| P3 | `app/Enums/DocumentPriorityEnum.php` | `tests/Unit/Enums/DocumentPriorityEnumTest.php` | ☐ |
| P3 | `app/Enums/EnumHasOptions.php` (via any enum using it) | `tests/Unit/Enums/EnumHasOptionsTest.php` | ☐ |
| — | all other enums (values only) | | — |
### Models
| Priority | Source | Test | Status |
|----------|--------|------|--------|
| P3 | `app/Models/Shift.php` (`scopeActive`, `scopeOpen`) | `tests/Feature/Models/ShiftTest.php` | ☐ |
| P3 | `app/Models/Medium.php` (`fullUrl`, `humanSize`, `exists`, `isPublic`, `isPrivate`, `deleteFile`) | `tests/Feature/Models/MediumTest.php` | ☐ |
| P3 | `app/Models/OtpCode.php` (`isExpired`) | `tests/Feature/Models/OtpCodeTest.php` | ☐ |
| — | all other models (relations / fillable only) | | — |
### Factories
Existing: `User`, `Pet`, `Document`, `DocumentAssignment`, `Medium`, `MediaAssignment`, `Report`.
Missing — create when first needed by a test:
| Model | Status |
|-------|--------|
| `Wallet` | ☑ |
| `WalletTransaction` | ☐ |
| `WithdrawRequest` | ☑ |
| `BankCard` | ☑ |
| `Invoice` | ☑ |
| `Payment` | ☑ |
| `OtpCode` | ☐ |
| `Shift` | ☐ |