871 lines
21 KiB
Plaintext
871 lines
21 KiB
Plaintext
---
|
|
description: Testing standards for Laravel API and Vue frontend
|
|
globs: ["**/tests/**", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "**/Test.php"]
|
|
alwaysApply: true
|
|
---
|
|
|
|
# Testing Standards
|
|
|
|
## Philosophy
|
|
|
|
1. **Test behavior, not implementation** - Focus on what, not how
|
|
2. **Feature tests for APIs** - Test full request/response cycles
|
|
3. **Unit tests for logic** - Test Actions and complex functions
|
|
4. **Integration tests for Vue** - Test component interactions
|
|
5. **Fast and isolated** - Each test should be independent
|
|
|
|
## Laravel Testing
|
|
|
|
### Directory Structure
|
|
|
|
```
|
|
api/tests/
|
|
├── Feature/
|
|
│ └── Api/
|
|
│ ├── AuthTest.php
|
|
│ ├── EventTest.php
|
|
│ ├── MemberTest.php
|
|
│ ├── MusicTest.php
|
|
│ ├── SetlistTest.php
|
|
│ ├── LocationTest.php
|
|
│ └── CustomerTest.php
|
|
├── Unit/
|
|
│ ├── Actions/
|
|
│ │ ├── CreateEventActionTest.php
|
|
│ │ └── ...
|
|
│ └── Models/
|
|
│ ├── EventTest.php
|
|
│ └── ...
|
|
└── TestCase.php
|
|
```
|
|
|
|
### Feature Test Template
|
|
|
|
```php
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Api;
|
|
|
|
use App\Enums\EventStatus;
|
|
use App\Models\Event;
|
|
use App\Models\Location;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
final class EventTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private User $admin;
|
|
private User $member;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->admin = User::factory()->admin()->create();
|
|
$this->member = User::factory()->member()->create();
|
|
}
|
|
|
|
// ==========================================
|
|
// INDEX
|
|
// ==========================================
|
|
|
|
public function test_guests_cannot_list_events(): void
|
|
{
|
|
$response = $this->getJson('/api/v1/events');
|
|
|
|
$response->assertStatus(401);
|
|
}
|
|
|
|
public function test_authenticated_users_can_list_events(): void
|
|
{
|
|
Event::factory()->count(3)->create();
|
|
|
|
$response = $this->actingAs($this->member)
|
|
->getJson('/api/v1/events');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonStructure([
|
|
'success',
|
|
'data' => [
|
|
'*' => ['id', 'title', 'event_date', 'status'],
|
|
],
|
|
'meta' => ['pagination'],
|
|
])
|
|
->assertJsonCount(3, 'data');
|
|
}
|
|
|
|
public function test_events_are_paginated(): void
|
|
{
|
|
Event::factory()->count(20)->create();
|
|
|
|
$response = $this->actingAs($this->member)
|
|
->getJson('/api/v1/events?per_page=10');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonCount(10, 'data')
|
|
->assertJsonPath('meta.pagination.total', 20)
|
|
->assertJsonPath('meta.pagination.per_page', 10);
|
|
}
|
|
|
|
// ==========================================
|
|
// SHOW
|
|
// ==========================================
|
|
|
|
public function test_can_view_single_event(): void
|
|
{
|
|
$event = Event::factory()->create(['title' => 'Summer Concert']);
|
|
|
|
$response = $this->actingAs($this->member)
|
|
->getJson("/api/v1/events/{$event->id}");
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('success', true)
|
|
->assertJsonPath('data.title', 'Summer Concert');
|
|
}
|
|
|
|
public function test_returns_404_for_nonexistent_event(): void
|
|
{
|
|
$response = $this->actingAs($this->member)
|
|
->getJson('/api/v1/events/nonexistent-id');
|
|
|
|
$response->assertStatus(404);
|
|
}
|
|
|
|
// ==========================================
|
|
// STORE
|
|
// ==========================================
|
|
|
|
public function test_admin_can_create_event(): void
|
|
{
|
|
$location = Location::factory()->create();
|
|
|
|
$eventData = [
|
|
'title' => 'New Year Concert',
|
|
'event_date' => now()->addMonth()->toDateString(),
|
|
'start_time' => '20:00',
|
|
'location_id' => $location->id,
|
|
'status' => 'draft',
|
|
];
|
|
|
|
$response = $this->actingAs($this->admin)
|
|
->postJson('/api/v1/events', $eventData);
|
|
|
|
$response->assertStatus(201)
|
|
->assertJsonPath('success', true)
|
|
->assertJsonPath('data.title', 'New Year Concert');
|
|
|
|
$this->assertDatabaseHas('events', [
|
|
'title' => 'New Year Concert',
|
|
'location_id' => $location->id,
|
|
]);
|
|
}
|
|
|
|
public function test_member_cannot_create_event(): void
|
|
{
|
|
$eventData = [
|
|
'title' => 'Unauthorized Event',
|
|
'event_date' => now()->addMonth()->toDateString(),
|
|
'start_time' => '20:00',
|
|
];
|
|
|
|
$response = $this->actingAs($this->member)
|
|
->postJson('/api/v1/events', $eventData);
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
public function test_validation_errors_are_returned(): void
|
|
{
|
|
$response = $this->actingAs($this->admin)
|
|
->postJson('/api/v1/events', []);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonPath('success', false)
|
|
->assertJsonValidationErrors(['title', 'event_date', 'start_time']);
|
|
}
|
|
|
|
public function test_event_date_must_be_future(): void
|
|
{
|
|
$response = $this->actingAs($this->admin)
|
|
->postJson('/api/v1/events', [
|
|
'title' => 'Past Event',
|
|
'event_date' => now()->subDay()->toDateString(),
|
|
'start_time' => '20:00',
|
|
]);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['event_date']);
|
|
}
|
|
|
|
// ==========================================
|
|
// UPDATE
|
|
// ==========================================
|
|
|
|
public function test_admin_can_update_event(): void
|
|
{
|
|
$event = Event::factory()->create(['title' => 'Old Title']);
|
|
|
|
$response = $this->actingAs($this->admin)
|
|
->putJson("/api/v1/events/{$event->id}", [
|
|
'title' => 'Updated Title',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('data.title', 'Updated Title');
|
|
|
|
$this->assertDatabaseHas('events', [
|
|
'id' => $event->id,
|
|
'title' => 'Updated Title',
|
|
]);
|
|
}
|
|
|
|
public function test_can_update_event_status(): void
|
|
{
|
|
$event = Event::factory()->draft()->create();
|
|
|
|
$response = $this->actingAs($this->admin)
|
|
->putJson("/api/v1/events/{$event->id}", [
|
|
'status' => 'confirmed',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('data.status', 'confirmed');
|
|
}
|
|
|
|
// ==========================================
|
|
// DELETE
|
|
// ==========================================
|
|
|
|
public function test_admin_can_delete_event(): void
|
|
{
|
|
$event = Event::factory()->create();
|
|
|
|
$response = $this->actingAs($this->admin)
|
|
->deleteJson("/api/v1/events/{$event->id}");
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('success', true);
|
|
|
|
$this->assertDatabaseMissing('events', ['id' => $event->id]);
|
|
}
|
|
|
|
public function test_member_cannot_delete_event(): void
|
|
{
|
|
$event = Event::factory()->create();
|
|
|
|
$response = $this->actingAs($this->member)
|
|
->deleteJson("/api/v1/events/{$event->id}");
|
|
|
|
$response->assertStatus(403);
|
|
$this->assertDatabaseHas('events', ['id' => $event->id]);
|
|
}
|
|
|
|
// ==========================================
|
|
// RELATIONSHIPS
|
|
// ==========================================
|
|
|
|
public function test_event_includes_location_when_loaded(): void
|
|
{
|
|
$location = Location::factory()->create(['name' => 'City Hall']);
|
|
$event = Event::factory()->create(['location_id' => $location->id]);
|
|
|
|
$response = $this->actingAs($this->member)
|
|
->getJson("/api/v1/events/{$event->id}");
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('data.location.name', 'City Hall');
|
|
}
|
|
}
|
|
```
|
|
|
|
### Unit Test Template (Action)
|
|
|
|
```php
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit\Actions;
|
|
|
|
use App\Actions\Events\CreateEventAction;
|
|
use App\Models\Event;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
final class CreateEventActionTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private CreateEventAction $action;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->action = new CreateEventAction();
|
|
}
|
|
|
|
public function test_creates_event_with_valid_data(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user);
|
|
|
|
$data = [
|
|
'title' => 'Test Event',
|
|
'event_date' => now()->addMonth()->toDateString(),
|
|
'start_time' => '20:00',
|
|
];
|
|
|
|
$event = $this->action->execute($data);
|
|
|
|
$this->assertInstanceOf(Event::class, $event);
|
|
$this->assertEquals('Test Event', $event->title);
|
|
$this->assertEquals($user->id, $event->created_by);
|
|
}
|
|
|
|
public function test_sets_default_values(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user);
|
|
|
|
$event = $this->action->execute([
|
|
'title' => 'Test',
|
|
'event_date' => now()->addMonth()->toDateString(),
|
|
'start_time' => '20:00',
|
|
]);
|
|
|
|
$this->assertEquals('EUR', $event->currency);
|
|
$this->assertEquals('draft', $event->status->value);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Model Factory Template
|
|
|
|
```php
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Database\Factories;
|
|
|
|
use App\Enums\EventStatus;
|
|
use App\Enums\EventVisibility;
|
|
use App\Models\Event;
|
|
use App\Models\Location;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
|
|
/**
|
|
* @extends Factory<Event>
|
|
*/
|
|
final class EventFactory extends Factory
|
|
{
|
|
protected $model = Event::class;
|
|
|
|
public function definition(): array
|
|
{
|
|
return [
|
|
'title' => fake()->sentence(3),
|
|
'description' => fake()->optional()->paragraph(),
|
|
'event_date' => fake()->dateTimeBetween('+1 week', '+6 months'),
|
|
'start_time' => fake()->time('H:i'),
|
|
'end_time' => fake()->optional()->time('H:i'),
|
|
'fee' => fake()->optional()->randomFloat(2, 100, 5000),
|
|
'currency' => 'EUR',
|
|
'status' => fake()->randomElement(EventStatus::cases()),
|
|
'visibility' => EventVisibility::Members,
|
|
'created_by' => User::factory(),
|
|
];
|
|
}
|
|
|
|
public function draft(): static
|
|
{
|
|
return $this->state(fn () => ['status' => EventStatus::Draft]);
|
|
}
|
|
|
|
public function confirmed(): static
|
|
{
|
|
return $this->state(fn () => ['status' => EventStatus::Confirmed]);
|
|
}
|
|
|
|
public function withLocation(): static
|
|
{
|
|
return $this->state(fn () => ['location_id' => Location::factory()]);
|
|
}
|
|
|
|
public function upcoming(): static
|
|
{
|
|
return $this->state(fn () => [
|
|
'event_date' => fake()->dateTimeBetween('+1 day', '+1 month'),
|
|
'status' => EventStatus::Confirmed,
|
|
]);
|
|
}
|
|
|
|
public function past(): static
|
|
{
|
|
return $this->state(fn () => [
|
|
'event_date' => fake()->dateTimeBetween('-6 months', '-1 day'),
|
|
'status' => EventStatus::Completed,
|
|
]);
|
|
}
|
|
}
|
|
```
|
|
|
|
### User Factory States
|
|
|
|
```php
|
|
<?php
|
|
|
|
// In UserFactory.php
|
|
|
|
public function admin(): static
|
|
{
|
|
return $this->state(fn () => [
|
|
'type' => 'member',
|
|
'role' => 'admin',
|
|
'status' => 'active',
|
|
]);
|
|
}
|
|
|
|
public function member(): static
|
|
{
|
|
return $this->state(fn () => [
|
|
'type' => 'member',
|
|
'role' => 'member',
|
|
'status' => 'active',
|
|
]);
|
|
}
|
|
|
|
public function bookingAgent(): static
|
|
{
|
|
return $this->state(fn () => [
|
|
'type' => 'member',
|
|
'role' => 'booking_agent',
|
|
'status' => 'active',
|
|
]);
|
|
}
|
|
|
|
public function customer(): static
|
|
{
|
|
return $this->state(fn () => [
|
|
'type' => 'customer',
|
|
'role' => null,
|
|
'status' => 'active',
|
|
]);
|
|
}
|
|
```
|
|
|
|
### Test Helpers (TestCase.php)
|
|
|
|
```php
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
|
|
|
abstract class TestCase extends BaseTestCase
|
|
{
|
|
/**
|
|
* Create and authenticate as admin user.
|
|
*/
|
|
protected function actingAsAdmin(): User
|
|
{
|
|
$admin = User::factory()->admin()->create();
|
|
$this->actingAs($admin);
|
|
return $admin;
|
|
}
|
|
|
|
/**
|
|
* Create and authenticate as regular member.
|
|
*/
|
|
protected function actingAsMember(): User
|
|
{
|
|
$member = User::factory()->member()->create();
|
|
$this->actingAs($member);
|
|
return $member;
|
|
}
|
|
|
|
/**
|
|
* Assert API success response structure.
|
|
*/
|
|
protected function assertApiSuccess($response, int $status = 200): void
|
|
{
|
|
$response->assertStatus($status)
|
|
->assertJsonStructure(['success', 'data'])
|
|
->assertJsonPath('success', true);
|
|
}
|
|
|
|
/**
|
|
* Assert API error response structure.
|
|
*/
|
|
protected function assertApiError($response, int $status = 400): void
|
|
{
|
|
$response->assertStatus($status)
|
|
->assertJsonPath('success', false)
|
|
->assertJsonStructure(['success', 'message']);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Running Tests
|
|
|
|
### Laravel
|
|
|
|
```bash
|
|
# Run all tests
|
|
cd api && php artisan test
|
|
|
|
# Run with coverage
|
|
php artisan test --coverage
|
|
|
|
# Run specific test file
|
|
php artisan test tests/Feature/Api/EventTest.php
|
|
|
|
# Run specific test method
|
|
php artisan test --filter test_admin_can_create_event
|
|
|
|
# Run in parallel
|
|
php artisan test --parallel
|
|
```
|
|
|
|
### Pest PHP Syntax
|
|
|
|
```php
|
|
<?php
|
|
|
|
use App\Models\Event;
|
|
use App\Models\User;
|
|
|
|
beforeEach(function () {
|
|
$this->admin = User::factory()->admin()->create();
|
|
});
|
|
|
|
it('allows admin to create events', function () {
|
|
$response = $this->actingAs($this->admin)
|
|
->postJson('/api/v1/events', [
|
|
'title' => 'Concert',
|
|
'event_date' => now()->addMonth()->toDateString(),
|
|
'start_time' => '20:00',
|
|
]);
|
|
|
|
$response->assertStatus(201);
|
|
expect(Event::count())->toBe(1);
|
|
});
|
|
|
|
it('validates required fields', function () {
|
|
$response = $this->actingAs($this->admin)
|
|
->postJson('/api/v1/events', []);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['title', 'event_date']);
|
|
});
|
|
|
|
it('returns paginated results', function () {
|
|
Event::factory()->count(25)->create();
|
|
|
|
$response = $this->actingAs($this->admin)
|
|
->getJson('/api/v1/events?per_page=10');
|
|
|
|
expect($response->json('data'))->toHaveCount(10);
|
|
expect($response->json('meta.pagination.total'))->toBe(25);
|
|
});
|
|
```
|
|
|
|
## Vue Testing (Vitest + Vue Test Utils)
|
|
|
|
### File Organization
|
|
|
|
```
|
|
src/
|
|
├── components/
|
|
│ └── EventCard/
|
|
│ ├── EventCard.vue
|
|
│ └── EventCard.test.ts
|
|
├── composables/
|
|
│ └── useEvents.test.ts
|
|
└── test/
|
|
├── setup.ts
|
|
├── mocks/
|
|
│ ├── handlers.ts # MSW handlers
|
|
│ └── server.ts
|
|
└── utils.ts # Custom render with providers
|
|
```
|
|
|
|
### Vitest Configuration
|
|
|
|
```typescript
|
|
// vitest.config.ts
|
|
import { defineConfig } from 'vitest/config'
|
|
import vue from '@vitejs/plugin-vue'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
export default defineConfig({
|
|
plugins: [vue()],
|
|
test: {
|
|
environment: 'jsdom',
|
|
globals: true,
|
|
setupFiles: ['./src/test/setup.ts'],
|
|
},
|
|
resolve: {
|
|
alias: {
|
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
### Test Setup
|
|
|
|
```typescript
|
|
// src/test/setup.ts
|
|
import { vi, beforeAll, afterAll, afterEach } from 'vitest'
|
|
import { config } from '@vue/test-utils'
|
|
import { server } from './mocks/server'
|
|
|
|
// Start MSW server
|
|
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
|
afterEach(() => server.resetHandlers())
|
|
afterAll(() => server.close())
|
|
|
|
// Global stubs for Vuexy components
|
|
config.global.stubs = {
|
|
VBtn: true,
|
|
VCard: true,
|
|
VCardText: true,
|
|
VCardTitle: true,
|
|
VTable: true,
|
|
VDialog: true,
|
|
VProgressCircular: true,
|
|
RouterLink: true,
|
|
}
|
|
```
|
|
|
|
### MSW Setup for API Mocking
|
|
|
|
```typescript
|
|
// src/test/mocks/handlers.ts
|
|
import { http, HttpResponse } from 'msw'
|
|
|
|
export const handlers = [
|
|
http.get('/api/v1/events', () => {
|
|
return HttpResponse.json({
|
|
success: true,
|
|
data: [
|
|
{ id: '1', title: 'Event 1', status: 'confirmed' },
|
|
{ id: '2', title: 'Event 2', status: 'draft' },
|
|
{ id: '3', title: 'Event 3', status: 'pending' },
|
|
],
|
|
meta: { pagination: { current_page: 1, total: 3, per_page: 15 } }
|
|
})
|
|
}),
|
|
|
|
http.post('/api/v1/events', async ({ request }) => {
|
|
const body = await request.json()
|
|
return HttpResponse.json({
|
|
success: true,
|
|
data: { id: '4', ...body },
|
|
message: 'Event created successfully'
|
|
}, { status: 201 })
|
|
}),
|
|
|
|
http.get('/api/v1/auth/user', () => {
|
|
return HttpResponse.json({
|
|
success: true,
|
|
data: { id: '1', name: 'Test User', email: 'test@example.com', role: 'admin' }
|
|
})
|
|
}),
|
|
]
|
|
|
|
// src/test/mocks/server.ts
|
|
import { setupServer } from 'msw/node'
|
|
import { handlers } from './handlers'
|
|
|
|
export const server = setupServer(...handlers)
|
|
```
|
|
|
|
### Test Utilities
|
|
|
|
```typescript
|
|
// src/test/utils.ts
|
|
import { mount, VueWrapper } from '@vue/test-utils'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'
|
|
import { createRouter, createWebHistory } from 'vue-router'
|
|
import type { Component } from 'vue'
|
|
|
|
export function createTestingPinia() {
|
|
const pinia = createPinia()
|
|
setActivePinia(pinia)
|
|
return pinia
|
|
}
|
|
|
|
export function createTestQueryClient() {
|
|
return new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false, gcTime: 0 },
|
|
mutations: { retry: false },
|
|
},
|
|
})
|
|
}
|
|
|
|
export function mountWithProviders(component: Component, options = {}) {
|
|
const pinia = createTestingPinia()
|
|
const queryClient = createTestQueryClient()
|
|
const router = createRouter({
|
|
history: createWebHistory(),
|
|
routes: [{ path: '/', component: { template: '<div />' } }],
|
|
})
|
|
|
|
return mount(component, {
|
|
global: {
|
|
plugins: [pinia, router, [VueQueryPlugin, { queryClient }]],
|
|
stubs: {
|
|
VBtn: true,
|
|
VCard: true,
|
|
VTable: true,
|
|
},
|
|
},
|
|
...options,
|
|
})
|
|
}
|
|
```
|
|
|
|
### Component Test Pattern
|
|
|
|
```typescript
|
|
// src/components/EventCard.test.ts
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { mount } from '@vue/test-utils'
|
|
import EventCard from './EventCard.vue'
|
|
|
|
describe('EventCard', () => {
|
|
const mockEvent = {
|
|
id: '1',
|
|
title: 'Summer Concert',
|
|
event_date: '2025-07-15',
|
|
status: 'confirmed',
|
|
status_label: 'Confirmed',
|
|
}
|
|
|
|
it('displays event title', () => {
|
|
const wrapper = mount(EventCard, {
|
|
props: { event: mockEvent },
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Summer Concert')
|
|
})
|
|
|
|
it('emits edit event when edit button clicked', async () => {
|
|
const wrapper = mount(EventCard, {
|
|
props: { event: mockEvent },
|
|
})
|
|
|
|
await wrapper.find('[data-test="edit-btn"]').trigger('click')
|
|
|
|
expect(wrapper.emitted('edit')).toBeTruthy()
|
|
expect(wrapper.emitted('edit')?.[0]).toEqual([mockEvent])
|
|
})
|
|
|
|
it('shows correct status color', () => {
|
|
const wrapper = mount(EventCard, {
|
|
props: { event: mockEvent },
|
|
})
|
|
|
|
const chip = wrapper.find('[data-test="status-chip"]')
|
|
expect(chip.classes()).toContain('bg-success')
|
|
})
|
|
})
|
|
```
|
|
|
|
### Composable Test Pattern
|
|
|
|
```typescript
|
|
// src/composables/__tests__/useEvents.test.ts
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { flushPromises } from '@vue/test-utils'
|
|
import { useEvents } from '../useEvents'
|
|
import { createTestQueryClient, createTestingPinia } from '@/test/utils'
|
|
import { VueQueryPlugin } from '@tanstack/vue-query'
|
|
import { createApp } from 'vue'
|
|
|
|
describe('useEvents', () => {
|
|
beforeEach(() => {
|
|
createTestingPinia()
|
|
})
|
|
|
|
it('fetches events successfully', async () => {
|
|
const app = createApp({ template: '<div />' })
|
|
const queryClient = createTestQueryClient()
|
|
app.use(VueQueryPlugin, { queryClient })
|
|
|
|
// Mount and test...
|
|
await flushPromises()
|
|
|
|
// Assertions
|
|
})
|
|
})
|
|
```
|
|
|
|
### Pest Expectations (Laravel)
|
|
|
|
```php
|
|
// Use fluent expectations
|
|
expect($user)->toBeInstanceOf(User::class);
|
|
expect($collection)->toHaveCount(5);
|
|
expect($response->json('data'))->toMatchArray([...]);
|
|
|
|
// Chain expectations
|
|
expect($user)
|
|
->name->toBe('John')
|
|
->email->toContain('@')
|
|
->created_at->toBeInstanceOf(Carbon::class);
|
|
```
|
|
|
|
## Test Coverage Goals
|
|
|
|
| Area | Target | Priority |
|
|
|------|--------|----------|
|
|
| API Endpoints | 90%+ | High |
|
|
| Actions | 100% | High |
|
|
| Models | 80%+ | Medium |
|
|
| Vue Composables | 80%+ | Medium |
|
|
| Vue Components | 60%+ | Low |
|
|
|
|
**Minimum Coverage**: 80% line coverage
|
|
|
|
## Coverage Requirements
|
|
|
|
- **Feature tests**: All API endpoints must have tests
|
|
- **Unit tests**: All Actions and Services with business logic
|
|
- **Component tests**: All interactive components
|
|
- **Composable tests**: All custom composables that fetch data
|
|
|
|
## Best Practices
|
|
|
|
### Do
|
|
|
|
- Test happy path AND edge cases
|
|
- Use descriptive test names
|
|
- One assertion focus per test
|
|
- Use factories for test data
|
|
- Clean up after tests (RefreshDatabase)
|
|
- Test authorization separately
|
|
|
|
### Don't
|
|
|
|
- Test framework code (Laravel, Vue)
|
|
- Test private methods directly
|
|
- Share state between tests
|
|
- Use real external APIs
|
|
- Over-mock (test real behavior)
|