Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Build Laravel 10+ apps with Eloquent, Sanctum auth, Horizon queues, Livewire, and RESTful API resources.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/testing.md
1# Testing23## Feature Tests45```php6namespace Tests\Feature;78use Tests\TestCase;9use App\Models\{User, Post};10use Illuminate\Foundation\Testing\RefreshDatabase;1112class PostTest extends TestCase13{14use RefreshDatabase;1516public function test_user_can_create_post(): void17{18$user = User::factory()->create();1920$response = $this->actingAs($user)->post('/api/posts', [21'title' => 'Test Post',22'content' => 'This is a test post content.',23]);2425$response->assertStatus(201)26->assertJson([27'data' => [28'title' => 'Test Post',29],30]);3132$this->assertDatabaseHas('posts', [33'title' => 'Test Post',34'user_id' => $user->id,35]);36}3738public function test_guest_cannot_create_post(): void39{40$response = $this->post('/api/posts', [41'title' => 'Test Post',42'content' => 'Content',43]);4445$response->assertStatus(401);46}4748public function test_post_requires_valid_data(): void49{50$user = User::factory()->create();5152$response = $this->actingAs($user)->post('/api/posts', [53'title' => 'AB', // Too short54]);5556$response->assertStatus(422)57->assertJsonValidationErrors(['title', 'content']);58}5960public function test_user_can_view_their_posts(): void61{62$user = User::factory()->create();63$posts = Post::factory()->count(3)->create(['user_id' => $user->id]);6465$response = $this->actingAs($user)->get('/api/posts');6667$response->assertStatus(200)68->assertJsonCount(3, 'data')69->assertJsonStructure([70'data' => [71'*' => ['id', 'title', 'content', 'created_at'],72],73]);74}7576public function test_user_can_update_own_post(): void77{78$user = User::factory()->create();79$post = Post::factory()->create(['user_id' => $user->id]);8081$response = $this->actingAs($user)->put("/api/posts/{$post->id}", [82'title' => 'Updated Title',83'content' => $post->content,84]);8586$response->assertStatus(200);8788$this->assertDatabaseHas('posts', [89'id' => $post->id,90'title' => 'Updated Title',91]);92}9394public function test_user_cannot_update_others_post(): void95{96$user = User::factory()->create();97$otherUser = User::factory()->create();98$post = Post::factory()->create(['user_id' => $otherUser->id]);99100$response = $this->actingAs($user)->put("/api/posts/{$post->id}", [101'title' => 'Updated Title',102]);103104$response->assertStatus(403);105}106}107```108109## Unit Tests110111```php112namespace Tests\Unit;113114use Tests\TestCase;115use App\Models\Post;116use App\Services\PostService;117use Illuminate\Foundation\Testing\RefreshDatabase;118119class PostServiceTest extends TestCase120{121use RefreshDatabase;122123public function test_generates_unique_slug(): void124{125$service = new PostService();126127$slug = $service->generateSlug('Test Post');128129$this->assertEquals('test-post', $slug);130}131132public function test_increments_slug_on_duplicate(): void133{134Post::factory()->create(['slug' => 'test-post']);135136$service = new PostService();137$slug = $service->generateSlug('Test Post');138139$this->assertEquals('test-post-1', $slug);140}141142public function test_post_excerpt_returns_limited_content(): void143{144$post = new Post(['content' => str_repeat('a', 200)]);145146$excerpt = $post->excerpt;147148$this->assertLessThanOrEqual(100, strlen($excerpt));149}150}151```152153## Pest PHP154155```php156<?php157158use App\Models\{User, Post};159160it('allows authenticated users to create posts', function () {161$user = User::factory()->create();162163$this->actingAs($user)164->post('/api/posts', [165'title' => 'Test Post',166'content' => 'Content',167])168->assertStatus(201);169170expect(Post::count())->toBe(1);171});172173it('prevents guests from creating posts', function () {174$this->post('/api/posts', [175'title' => 'Test Post',176'content' => 'Content',177])->assertStatus(401);178});179180test('post requires title and content', function () {181$user = User::factory()->create();182183$this->actingAs($user)184->post('/api/posts', [])185->assertJsonValidationErrors(['title', 'content']);186});187188// Datasets189it('validates title length', function (string $title, bool $shouldPass) {190$user = User::factory()->create();191192$response = $this->actingAs($user)->post('/api/posts', [193'title' => $title,194'content' => 'Content',195]);196197if ($shouldPass) {198$response->assertStatus(201);199} else {200$response->assertJsonValidationErrors(['title']);201}202})->with([203['AB', false], // Too short204['ABC', true], // Minimum valid205[str_repeat('A', 255), true], // Maximum valid206[str_repeat('A', 256), false], // Too long207]);208209// Hooks210beforeEach(function () {211$this->user = User::factory()->create();212});213214afterEach(function () {215// Cleanup216});217```218219## Factories220221```php222namespace Database\Factories;223224use App\Models\{User, Category};225use Illuminate\Database\Eloquent\Factories\Factory;226227class PostFactory extends Factory228{229public function definition(): array230{231return [232'title' => fake()->sentence(),233'slug' => fake()->slug(),234'content' => fake()->paragraphs(3, true),235'excerpt' => fake()->text(100),236'published_at' => fake()->dateTimeBetween('-1 year', 'now'),237'user_id' => User::factory(),238'category_id' => Category::factory(),239];240}241242public function unpublished(): static243{244return $this->state(fn (array $attributes) => [245'published_at' => null,246]);247}248249public function published(): static250{251return $this->state(fn (array $attributes) => [252'published_at' => now(),253]);254}255256public function forUser(User $user): static257{258return $this->state(fn (array $attributes) => [259'user_id' => $user->id,260]);261}262263public function configure(): static264{265return $this->afterCreating(function (Post $post) {266$post->tags()->attach(267Tag::factory()->count(3)->create()268);269});270}271}272273// Usage274$post = Post::factory()->create();275$unpublished = Post::factory()->unpublished()->create();276$posts = Post::factory()->count(10)->create();277$userPosts = Post::factory()->forUser($user)->count(5)->create();278279// With relationships280$post = Post::factory()281->has(Comment::factory()->count(3))282->create();283284// For relationship285$posts = Post::factory()286->count(3)287->for($user)288->create();289```290291## Mocking292293```php294use App\Services\ExternalApiService;295use Illuminate\Support\Facades\Http;296297public function test_fetches_data_from_external_api(): void298{299Http::fake([300'api.example.com/*' => Http::response([301'data' => ['id' => 1, 'name' => 'Test'],302], 200),303]);304305$service = new ExternalApiService();306$result = $service->fetchData();307308$this->assertEquals('Test', $result['name']);309310Http::assertSent(function ($request) {311return $request->url() === 'https://api.example.com/data' &&312$request->hasHeader('Authorization');313});314}315316// Mock events317use Illuminate\Support\Facades\Event;318319Event::fake([PostCreated::class]);320321// Test code that dispatches events322323Event::assertDispatched(PostCreated::class, function ($event) {324return $event->post->id === 1;325});326327// Mock queues328use Illuminate\Support\Facades\Queue;329330Queue::fake();331332// Test code that dispatches jobs333334Queue::assertPushed(ProcessPost::class);335Queue::assertPushed(ProcessPost::class, 2);336Queue::assertPushed(ProcessPost::class, function ($job) {337return $job->post->id === 1;338});339340// Mock notifications341use Illuminate\Support\Facades\Notification;342343Notification::fake();344345// Test code that sends notifications346347Notification::assertSentTo($user, PostPublished::class);348349// Mock storage350use Illuminate\Support\Facades\Storage;351352Storage::fake('public');353354// Test file upload355356Storage::disk('public')->assertExists('file.jpg');357Storage::disk('public')->assertMissing('missing.jpg');358```359360## Database Testing361362```php363use Illuminate\Foundation\Testing\RefreshDatabase;364use Illuminate\Foundation\Testing\DatabaseTransactions;365366class PostTest extends TestCase367{368use RefreshDatabase; // Migrate database before each test369370// Or use transactions371use DatabaseTransactions; // Rollback after each test372373public function test_database_assertions(): void374{375$post = Post::factory()->create([376'title' => 'Test Post',377]);378379$this->assertDatabaseHas('posts', [380'title' => 'Test Post',381]);382383$post->delete();384385$this->assertDatabaseMissing('posts', [386'id' => $post->id,387]);388389$this->assertSoftDeleted('posts', [390'id' => $post->id,391]);392}393394public function test_model_exists(): void395{396$post = Post::factory()->create();397398$this->assertModelExists($post);399400$post->delete();401402$this->assertModelMissing($post);403}404}405```406407## API Testing408409```php410public function test_api_returns_paginated_posts(): void411{412Post::factory()->count(30)->create();413414$response = $this->get('/api/posts');415416$response->assertStatus(200)417->assertJsonStructure([418'data' => [419'*' => ['id', 'title', 'content'],420],421'meta' => ['total', 'current_page', 'last_page'],422'links' => ['first', 'last', 'prev', 'next'],423])424->assertJsonCount(15, 'data'); // Default per page425}426427public function test_api_filters_posts_by_category(): void428{429$category = Category::factory()->create();430Post::factory()->count(5)->create(['category_id' => $category->id]);431Post::factory()->count(5)->create();432433$response = $this->get("/api/posts?category={$category->id}");434435$response->assertJsonCount(5, 'data')436->assertJson([437'data' => [438['category_id' => $category->id],439],440]);441}442```443444## Authentication Testing445446```php447use Laravel\Sanctum\Sanctum;448449public function test_authenticated_user_can_access_endpoint(): void450{451$user = User::factory()->create();452453Sanctum::actingAs($user, ['*']);454455$response = $this->get('/api/user');456457$response->assertStatus(200)458->assertJson([459'data' => [460'id' => $user->id,461'email' => $user->email,462],463]);464}465466public function test_user_with_wrong_ability_cannot_access(): void467{468$user = User::factory()->create();469470Sanctum::actingAs($user, ['view-posts']);471472$response = $this->post('/api/posts', [473'title' => 'Test',474'content' => 'Content',475]);476477$response->assertStatus(403);478}479```480481## Running Tests482483```bash484# Run all tests485php artisan test486487# Run specific test488php artisan test --filter=test_user_can_create_post489490# Run test file491php artisan test tests/Feature/PostTest.php492493# Parallel testing494php artisan test --parallel495496# With coverage497php artisan test --coverage498499# Coverage minimum500php artisan test --coverage --min=80501502# Stop on failure503php artisan test --stop-on-failure504505# Pest specific506./vendor/bin/pest507./vendor/bin/pest --filter=PostTest508./vendor/bin/pest --coverage509```510511## Best Practices5125131. **Use RefreshDatabase** - Clean database for each test5142. **Use factories** - Don't manually create test data5153. **Test one thing** - Each test should verify one behavior5164. **Use descriptive names** - test_user_can_create_post5175. **AAA pattern** - Arrange, Act, Assert5186. **Mock external services** - Don't make real API calls5197. **Fake queues and events** - Test async code synchronously5208. **Test edge cases** - Invalid data, permissions, etc.5219. **Achieve >85% coverage** - Test critical paths52210. **Run tests in CI/CD** - Automate test execution523