# Testing Guide Comprehensive guide to testing Core PHP Framework applications. ## Running Tests ```bash # Run all tests composer test # Run specific test file ./vendor/bin/phpunit packages/core-php/tests/Feature/ActivityLogServiceTest.php # Run tests with coverage ./vendor/bin/phpunit --coverage-html coverage # Run specific test method ./vendor/bin/phpunit --filter test_creates_post ``` ## Test Structure ``` tests/ ├── Feature/ # Integration tests │ ├── ApiTest.php │ ├── AuthTest.php │ └── PostTest.php ├── Unit/ # Unit tests │ ├── ActionTest.php │ └── ServiceTest.php └── TestCase.php # Base test case ``` ## Writing Feature Tests Feature tests test complete workflows: ```php create(); $response = $this->actingAs($user) ->post('/posts', [ 'title' => 'Test Post', 'content' => 'Test content', 'status' => 'draft', ]); $response->assertRedirect(); $this->assertDatabaseHas('posts', [ 'title' => 'Test Post', 'author_id' => $user->id, ]); } public function test_guest_cannot_create_post(): void { $response = $this->post('/posts', [ 'title' => 'Test Post', 'content' => 'Test content', ]); $response->assertRedirect(route('login')); } public function test_user_can_view_own_posts(): void { $user = User::factory()->create(); $post = Post::factory()->create(['author_id' => $user->id]); $response = $this->actingAs($user) ->get("/posts/{$post->id}"); $response->assertOk(); $response->assertSee($post->title); } } ``` ## Writing Unit Tests Unit tests test isolated components: ```php 'Test Post', 'content' => 'Test content', 'status' => 'draft', ]); $this->assertInstanceOf(Post::class, $post); $this->assertEquals('Test Post', $post->title); $this->assertDatabaseHas('posts', ['id' => $post->id]); } public function test_generates_slug_from_title(): void { $post = CreatePost::run([ 'title' => 'Test Post', 'content' => 'Content', ]); $this->assertEquals('test-post', $post->slug); } public function test_throws_exception_for_invalid_data(): void { $this->expectException(ValidationException::class); CreatePost::run([ 'title' => '', // Invalid 'content' => 'Content', ]); } } ``` ## Database Testing ### Factories ```php $this->faker->sentence(), 'content' => $this->faker->paragraphs(3, true), 'status' => 'draft', 'author_id' => User::factory(), ]; } public function published(): self { return $this->state([ 'status' => 'published', 'published_at' => now(), ]); } public function draft(): self { return $this->state(['status' => 'draft']); } } ``` **Usage:** ```php // Create single post $post = Post::factory()->create(); // Create published post $post = Post::factory()->published()->create(); // Create multiple posts $posts = Post::factory()->count(10)->create(); // Create with specific attributes $post = Post::factory()->create([ 'title' => 'Specific Title', ]); ``` ### Database Assertions ```php // Assert record exists $this->assertDatabaseHas('posts', [ 'title' => 'Test Post', 'status' => 'published', ]); // Assert record doesn't exist $this->assertDatabaseMissing('posts', [ 'title' => 'Deleted Post', ]); // Assert record count $this->assertDatabaseCount('posts', 10); // Assert model exists $this->assertModelExists($post); // Assert model deleted $this->assertSoftDeleted($post); ``` ## API Testing ```php create(); Sanctum::actingAs($user, ['posts:read']); Post::factory()->count(5)->published()->create(); $response = $this->getJson('/api/v1/posts'); $response->assertOk(); $response->assertJsonCount(5, 'data'); $response->assertJsonStructure([ 'data' => [ '*' => ['id', 'title', 'status', 'created_at'], ], ]); } public function test_creates_post(): void { $user = User::factory()->create(); Sanctum::actingAs($user, ['posts:write']); $response = $this->postJson('/api/v1/posts', [ 'title' => 'API Test Post', 'content' => 'Test content', ]); $response->assertCreated(); $response->assertJson([ 'title' => 'API Test Post', ]); $this->assertDatabaseHas('posts', [ 'title' => 'API Test Post', ]); } public function test_requires_authentication(): void { $response = $this->getJson('/api/v1/posts'); $response->assertUnauthorized(); } public function test_requires_correct_scope(): void { $user = User::factory()->create(); Sanctum::actingAs($user, ['posts:read']); // Missing write scope $response = $this->postJson('/api/v1/posts', [ 'title' => 'Test', 'content' => 'Content', ]); $response->assertForbidden(); } } ``` ## Livewire Testing ```php create(); Livewire::test(PostEditor::class, ['post' => $post]) ->assertSee($post->title) ->assertSee('Save'); } public function test_updates_post(): void { $post = Post::factory()->create(['title' => 'Original']); Livewire::test(PostEditor::class, ['post' => $post]) ->set('title', 'Updated Title') ->call('save') ->assertDispatched('post-updated'); $this->assertEquals('Updated Title', $post->fresh()->title); } public function test_validates_input(): void { $post = Post::factory()->create(); Livewire::test(PostEditor::class, ['post' => $post]) ->set('title', '') ->call('save') ->assertHasErrors(['title' => 'required']); } } ``` ## Mocking ### Mocking Services ```php use Mockery; use Mod\Payment\Services\PaymentService; public function test_processes_order_with_mock(): void { $mock = Mockery::mock(PaymentService::class); $mock->shouldReceive('charge') ->once() ->with(1000, 'GBP') ->andReturn(new PaymentResult(success: true)); $this->app->instance(PaymentService::class, $mock); $order = Order::factory()->create(); $result = $this->orderService->process($order); $this->assertTrue($result->success); } ``` ### Mocking Facades ```php use Illuminate\Support\Facades\Storage; public function test_uploads_file(): void { Storage::fake('s3'); $this->post('/upload', [ 'file' => UploadedFile::fake()->image('photo.jpg'), ]); Storage::disk('s3')->assertExists('photos/photo.jpg'); } ``` ### Mocking Events ```php use Illuminate\Support\Facades\Event; use Mod\Blog\Events\PostPublished; public function test_fires_event(): void { Event::fake([PostPublished::class]); $post = Post::factory()->create(); $service->publish($post); Event::assertDispatched(PostPublished::class, function ($event) use ($post) { return $event->post->id === $post->id; }); } ``` ## Testing Workspace Isolation ```php public function test_scopes_to_workspace(): void { $workspace1 = Workspace::factory()->create(); $workspace2 = Workspace::factory()->create(); $post1 = Post::factory()->create(['workspace_id' => $workspace1->id]); $post2 = Post::factory()->create(['workspace_id' => $workspace2->id]); // Acting as user in workspace1 $user = User::factory()->create(['workspace_id' => $workspace1->id]); $posts = Post::all(); // Should only see workspace1's posts $this->assertCount(1, $posts); $this->assertEquals($post1->id, $posts->first()->id); } ``` ## Best Practices ### 1. Test One Thing ```php // ✅ Good - tests one behavior public function test_creates_post(): void { $post = CreatePost::run([...]); $this->assertInstanceOf(Post::class, $post); } // ❌ Bad - tests multiple things public function test_post_operations(): void { $post = CreatePost::run([...]); $this->assertInstanceOf(Post::class, $post); $post->publish(); $this->assertEquals('published', $post->status); $post->delete(); $this->assertSoftDeleted($post); } ``` ### 2. Use Descriptive Names ```php // ✅ Good public function test_user_can_create_post_with_valid_data(): void // ❌ Bad public function test_create(): void ``` ### 3. Arrange, Act, Assert ```php public function test_publishes_post(): void { // Arrange $post = Post::factory()->create(['status' => 'draft']); $user = User::factory()->create(); // Act $result = $service->publish($post, $user); // Assert $this->assertEquals('published', $result->status); $this->assertNotNull($result->published_at); } ``` ### 4. Clean Up After Tests ```php use Illuminate\Foundation\Testing\RefreshDatabase; class PostTest extends TestCase { use RefreshDatabase; // Resets database after each test public function test_something(): void { // Test code } } ``` ## Learn More - [Actions Pattern →](/patterns-guide/actions) - [Service Pattern →](/patterns-guide/services) - [Contributing →](/contributing)