205 lines
14 KiB
Markdown
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` | ☐ |
|