Lección 40 de 45 18 min de lectura

Feature Tests

Los Feature tests permiten probar funcionalidades completas de tu aplicación, simulando peticiones HTTP y verificando respuestas, redirecciones, contenido y cambios en la base de datos.

¿Qué son los Feature tests?

Los Feature tests (también llamados tests funcionales o de integración) prueban cómo funcionan las diferentes partes de tu aplicación trabajando juntas. A diferencia de los Unit tests que prueban piezas aisladas, los Feature tests hacen peticiones HTTP directamente a tu aplicación y verifican las respuestas.

Importante: Los Feature tests no usan un navegador real ni ejecutan JavaScript. Para probar interacciones reales del usuario (clics, formularios dinámicos, JavaScript) necesitas Browser tests con Laravel Dusk. Los Feature tests son más rápidos y suficientes para la mayoría de casos.

Con los Feature tests puedes verificar:

  • Que las rutas responden correctamente
  • Que los formularios validan y guardan datos
  • Que la autenticación funciona como se espera
  • Que las políticas de autorización se aplican
  • Que las vistas muestran el contenido correcto

Crear un Feature test

Usa Artisan para generar un nuevo Feature test:

bash
# Crear Feature test (por defecto)
php artisan make:test PostTest

# Se crea en tests/Feature/PostTest.php

Estructura básica de un Feature test:

php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostTest extends TestCase
{
    use RefreshDatabase;

    public function test_example(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

Nota: Los Feature tests extienden Tests\TestCase, que a su vez extiende la clase base de Laravel. Esto les da acceso a métodos como $this->get(), $this->post(), etc.

Hacer peticiones HTTP

Laravel proporciona métodos para simular todos los tipos de peticiones HTTP:

php
<?php

// GET - Obtener recursos
$response = $this->get('/posts');
$response = $this->get('/posts/1');

// POST - Crear recursos
$response = $this->post('/posts', [
    'title' => 'Mi post',
    'content' => 'Contenido del post',
]);

// PUT/PATCH - Actualizar recursos
$response = $this->put('/posts/1', [
    'title' => 'Título actualizado',
]);
$response = $this->patch('/posts/1', [
    'title' => 'Título actualizado',
]);

// DELETE - Eliminar recursos
$response = $this->delete('/posts/1');

// Con headers personalizados
$response = $this->withHeaders([
    'X-Custom-Header' => 'value',
])->get('/api/posts');

// Con cookies
$response = $this->withCookies([
    'theme' => 'dark',
])->get('/dashboard');

Probar rutas y respuestas

El caso más básico es verificar que las rutas responden con el código de estado correcto:

php
<?php

namespace Tests\Feature;

use Tests\TestCase;

class RouteTest extends TestCase
{
    public function test_home_page_is_accessible(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
        // O usando el helper
        $response->assertOk();
    }

    public function test_about_page_exists(): void
    {
        $response = $this->get('/about');

        $response->assertOk();
        $response->assertSee('Sobre nosotros');
    }

    public function test_nonexistent_page_returns_404(): void
    {
        $response = $this->get('/pagina-que-no-existe');

        $response->assertNotFound();
    }
}

Probar vistas

Verifica que se renderiza la vista correcta y que contiene los datos esperados:

php
<?php

namespace Tests\Feature;

use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostViewTest extends TestCase
{
    use RefreshDatabase;

    public function test_posts_index_uses_correct_view(): void
    {
        $response = $this->get('/posts');

        $response->assertViewIs('posts.index');
    }

    public function test_posts_index_has_posts_variable(): void
    {
        Post::factory()->count(3)->create();

        $response = $this->get('/posts');

        $response->assertViewHas('posts');
    }

    public function test_posts_index_shows_post_titles(): void
    {
        $post = Post::factory()->create(['title' => 'Mi primer post']);

        $response = $this->get('/posts');

        $response->assertSee('Mi primer post');
    }

    public function test_post_show_displays_content(): void
    {
        $post = Post::factory()->create([
            'title' => 'Post de prueba',
            'content' => 'Este es el contenido del post',
        ]);

        $response = $this->get("/posts/{$post->id}");

        $response->assertOk();
        $response->assertSee('Post de prueba');
        $response->assertSee('Este es el contenido del post');
    }

    public function test_view_has_correct_post_count(): void
    {
        Post::factory()->count(5)->create();

        $response = $this->get('/posts');

        $response->assertViewHas('posts', function ($posts) {
            return $posts->count() === 5;
        });
    }
}

Probar formularios y validación

Los tests de formularios verifican que los datos se validan y guardan correctamente:

php
<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostFormTest extends TestCase
{
    use RefreshDatabase;

    private User $user;

    protected function setUp(): void
    {
        parent::setUp();
        $this->user = User::factory()->create();
    }

    public function test_create_form_is_displayed(): void
    {
        $response = $this->actingAs($this->user)->get('/posts/create');

        $response->assertOk();
        $response->assertSee('Crear post');
    }

    public function test_can_create_post_with_valid_data(): void
    {
        $response = $this->actingAs($this->user)->post('/posts', [
            'title' => 'Nuevo post',
            'content' => 'Contenido del post',
        ]);

        $response->assertRedirect('/posts');
        $this->assertDatabaseHas('posts', [
            'title' => 'Nuevo post',
            'user_id' => $this->user->id,
        ]);
    }

    public function test_title_is_required(): void
    {
        $response = $this->actingAs($this->user)->post('/posts', [
            'title' => '',
            'content' => 'Contenido',
        ]);

        $response->assertSessionHasErrors('title');
        $this->assertDatabaseCount('posts', 0);
    }

    public function test_title_must_be_at_least_3_characters(): void
    {
        $response = $this->actingAs($this->user)->post('/posts', [
            'title' => 'AB',
            'content' => 'Contenido',
        ]);

        $response->assertSessionHasErrors('title');
    }

    public function test_content_is_required(): void
    {
        $response = $this->actingAs($this->user)->post('/posts', [
            'title' => 'Título válido',
            'content' => '',
        ]);

        $response->assertSessionHasErrors('content');
    }

    public function test_validation_errors_are_displayed(): void
    {
        $response = $this->actingAs($this->user)
            ->from('/posts/create')
            ->post('/posts', [
                'title' => '',
                'content' => '',
            ]);

        $response->assertRedirect('/posts/create');
        $response->assertSessionHasErrors(['title', 'content']);
    }

    public function test_old_input_is_preserved_on_validation_error(): void
    {
        $response = $this->actingAs($this->user)->post('/posts', [
            'title' => 'Título válido',
            'content' => '',
        ]);

        $response->assertSessionHasInput('title', 'Título válido');
    }
}

Probar autenticación

Verifica que las rutas protegidas funcionan correctamente y que el proceso de login/logout es correcto:

php
<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class AuthenticationTest extends TestCase
{
    use RefreshDatabase;

    public function test_guest_is_redirected_from_dashboard(): void
    {
        $response = $this->get('/dashboard');

        $response->assertRedirect('/login');
    }

    public function test_user_can_access_dashboard(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->get('/dashboard');

        $response->assertOk();
    }

    public function test_login_page_is_accessible(): void
    {
        $response = $this->get('/login');

        $response->assertOk();
        $response->assertViewIs('auth.login');
    }

    public function test_user_can_login_with_correct_credentials(): void
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password123'),
        ]);

        $response = $this->post('/login', [
            'email' => 'test@example.com',
            'password' => 'password123',
        ]);

        $response->assertRedirect('/dashboard');
        $this->assertAuthenticatedAs($user);
    }

    public function test_user_cannot_login_with_wrong_password(): void
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password123'),
        ]);

        $response = $this->post('/login', [
            'email' => 'test@example.com',
            'password' => 'wrong-password',
        ]);

        $response->assertSessionHasErrors('email');
        $this->assertGuest();
    }

    public function test_user_can_logout(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/logout');

        $response->assertRedirect('/');
        $this->assertGuest();
    }

    public function test_authenticated_user_cannot_access_login(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->get('/login');

        $response->assertRedirect('/dashboard');
    }
}

Probar autorización

Verifica que las políticas y gates funcionan correctamente:

php
<?php

namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostAuthorizationTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_edit_own_post(): void
    {
        $user = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $user->id]);

        $response = $this->actingAs($user)->get("/posts/{$post->id}/edit");

        $response->assertOk();
    }

    public function test_user_cannot_edit_others_post(): void
    {
        $user = User::factory()->create();
        $otherUser = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $otherUser->id]);

        $response = $this->actingAs($user)->get("/posts/{$post->id}/edit");

        $response->assertForbidden();
    }

    public function test_user_can_update_own_post(): void
    {
        $user = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $user->id]);

        $response = $this->actingAs($user)->put("/posts/{$post->id}", [
            'title' => 'Título actualizado',
            'content' => 'Contenido actualizado',
        ]);

        $response->assertRedirect();
        $this->assertDatabaseHas('posts', [
            'id' => $post->id,
            'title' => 'Título actualizado',
        ]);
    }

    public function test_user_cannot_update_others_post(): void
    {
        $user = User::factory()->create();
        $otherUser = User::factory()->create();
        $post = Post::factory()->create([
            'user_id' => $otherUser->id,
            'title' => 'Título original',
        ]);

        $response = $this->actingAs($user)->put("/posts/{$post->id}", [
            'title' => 'Intento de hackeo',
        ]);

        $response->assertForbidden();
        $this->assertDatabaseHas('posts', [
            'id' => $post->id,
            'title' => 'Título original',
        ]);
    }

    public function test_user_can_delete_own_post(): void
    {
        $user = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $user->id]);

        $response = $this->actingAs($user)->delete("/posts/{$post->id}");

        $response->assertRedirect('/posts');
        $this->assertDatabaseMissing('posts', ['id' => $post->id]);
    }

    public function test_admin_can_edit_any_post(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $user = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $user->id]);

        $response = $this->actingAs($admin)->get("/posts/{$post->id}/edit");

        $response->assertOk();
    }
}

Probar redirecciones

Verifica que las redirecciones funcionan correctamente:

php
<?php

namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class RedirectTest extends TestCase
{
    use RefreshDatabase;

    public function test_redirects_to_posts_after_create(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/posts', [
            'title' => 'Nuevo post',
            'content' => 'Contenido',
        ]);

        $response->assertRedirect('/posts');
    }

    public function test_redirects_to_named_route(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/posts', [
            'title' => 'Nuevo post',
            'content' => 'Contenido',
        ]);

        $response->assertRedirectToRoute('posts.index');
    }

    public function test_redirects_to_post_after_update(): void
    {
        $user = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $user->id]);

        $response = $this->actingAs($user)->put("/posts/{$post->id}", [
            'title' => 'Actualizado',
            'content' => 'Contenido',
        ]);

        $response->assertRedirect("/posts/{$post->id}");
    }

    public function test_redirect_contains_success_message(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/posts', [
            'title' => 'Nuevo post',
            'content' => 'Contenido',
        ]);

        $response->assertSessionHas('success', 'Post creado correctamente');
    }
}

Probar sesión y mensajes flash

Verifica los datos de sesión y mensajes flash:

php
<?php

namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class SessionTest extends TestCase
{
    use RefreshDatabase;

    public function test_success_message_after_post_creation(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/posts', [
            'title' => 'Nuevo post',
            'content' => 'Contenido',
        ]);

        $response->assertSessionHas('success');
    }

    public function test_error_message_when_post_not_found(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->get('/posts/999');

        $response->assertNotFound();
    }

    public function test_session_has_no_errors_on_success(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/posts', [
            'title' => 'Título válido',
            'content' => 'Contenido válido',
        ]);

        $response->assertSessionHasNoErrors();
    }

    public function test_session_has_specific_error_message(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/posts', [
            'title' => '',
            'content' => 'Contenido',
        ]);

        $response->assertSessionHasErrors([
            'title' => 'El campo título es obligatorio.',
        ]);
    }

    public function test_can_set_session_before_request(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)
            ->withSession(['cart' => ['item1', 'item2']])
            ->get('/checkout');

        $response->assertOk();
    }
}

Probar subida de archivos

Laravel facilita probar la subida de archivos con archivos falsos:

php
<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class FileUploadTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_upload_avatar(): void
    {
        Storage::fake('public');
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/profile/avatar', [
            'avatar' => UploadedFile::fake()->image('avatar.jpg', 200, 200),
        ]);

        $response->assertRedirect();
        Storage::disk('public')->assertExists('avatars/' . $user->id . '.jpg');
    }

    public function test_avatar_must_be_an_image(): void
    {
        Storage::fake('public');
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/profile/avatar', [
            'avatar' => UploadedFile::fake()->create('document.pdf', 100),
        ]);

        $response->assertSessionHasErrors('avatar');
        Storage::disk('public')->assertMissing('avatars/' . $user->id . '.jpg');
    }

    public function test_avatar_max_size_is_2mb(): void
    {
        Storage::fake('public');
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/profile/avatar', [
            'avatar' => UploadedFile::fake()->image('big.jpg')->size(3000),
        ]);

        $response->assertSessionHasErrors('avatar');
    }

    public function test_can_upload_multiple_images(): void
    {
        Storage::fake('public');
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/gallery', [
            'images' => [
                UploadedFile::fake()->image('photo1.jpg'),
                UploadedFile::fake()->image('photo2.jpg'),
                UploadedFile::fake()->image('photo3.jpg'),
            ],
        ]);

        $response->assertRedirect();
        $this->assertDatabaseCount('images', 3);
    }
}

Probar con diferentes usuarios

Usa actingAs() para simular diferentes tipos de usuarios:

php
<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class RoleBasedAccessTest extends TestCase
{
    use RefreshDatabase;

    public function test_admin_can_access_admin_panel(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)->get('/admin');

        $response->assertOk();
        $response->assertSee('Panel de Administración');
    }

    public function test_regular_user_cannot_access_admin_panel(): void
    {
        $user = User::factory()->create(['role' => 'user']);

        $response = $this->actingAs($user)->get('/admin');

        $response->assertForbidden();
    }

    public function test_moderator_can_moderate_posts(): void
    {
        $moderator = User::factory()->create(['role' => 'moderator']);

        $response = $this->actingAs($moderator)->get('/moderation');

        $response->assertOk();
    }

    public function test_premium_user_sees_premium_content(): void
    {
        $premium = User::factory()->create(['is_premium' => true]);

        $response = $this->actingAs($premium)->get('/premium-content');

        $response->assertOk();
        $response->assertSee('Contenido exclusivo');
    }

    public function test_free_user_sees_upgrade_prompt(): void
    {
        $free = User::factory()->create(['is_premium' => false]);

        $response = $this->actingAs($free)->get('/premium-content');

        $response->assertOk();
        $response->assertSee('Actualiza a Premium');
    }
}

Ejemplo completo: Test de un flujo HTTP

Veamos cómo testear un flujo completo de registro y creación de contenido mediante peticiones HTTP:

php
<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserJourneyTest extends TestCase
{
    use RefreshDatabase;

    public function test_complete_user_registration_flow(): void
    {
        // 1. Usuario visita página de registro
        $response = $this->get('/register');
        $response->assertOk();
        $response->assertSee('Crear cuenta');

        // 2. Usuario envía formulario de registro
        $response = $this->post('/register', [
            'name' => 'Juan García',
            'email' => 'juan@example.com',
            'password' => 'password123',
            'password_confirmation' => 'password123',
        ]);

        // 3. Verificar que el usuario fue creado
        $this->assertDatabaseHas('users', [
            'name' => 'Juan García',
            'email' => 'juan@example.com',
        ]);

        // 4. Usuario es redirigido y autenticado
        $response->assertRedirect('/dashboard');
        $this->assertAuthenticated();

        // 5. Usuario puede acceder al dashboard
        $user = User::where('email', 'juan@example.com')->first();
        $response = $this->actingAs($user)->get('/dashboard');
        $response->assertOk();
        $response->assertSee('Bienvenido, Juan García');
    }

    public function test_user_can_create_and_view_post(): void
    {
        $user = User::factory()->create();

        // 1. Usuario accede al formulario de creación
        $response = $this->actingAs($user)->get('/posts/create');
        $response->assertOk();

        // 2. Usuario crea un post
        $response = $this->actingAs($user)->post('/posts', [
            'title' => 'Mi primer post',
            'content' => 'Este es el contenido de mi post.',
        ]);

        // 3. Verificar redirección con mensaje de éxito
        $response->assertRedirect('/posts');
        $response->assertSessionHas('success');

        // 4. Verificar que el post existe en BD
        $this->assertDatabaseHas('posts', [
            'title' => 'Mi primer post',
            'user_id' => $user->id,
        ]);

        // 5. El post aparece en el listado
        $response = $this->get('/posts');
        $response->assertSee('Mi primer post');

        // 6. Usuario puede ver el detalle del post
        $post = $user->posts()->first();
        $response = $this->get("/posts/{$post->id}");
        $response->assertOk();
        $response->assertSee('Mi primer post');
        $response->assertSee('Este es el contenido de mi post.');
    }

    public function test_user_can_edit_and_delete_own_post(): void
    {
        $user = User::factory()->create();

        // Crear post
        $this->actingAs($user)->post('/posts', [
            'title' => 'Post original',
            'content' => 'Contenido original',
        ]);

        $post = $user->posts()->first();

        // Editar post
        $response = $this->actingAs($user)->put("/posts/{$post->id}", [
            'title' => 'Post editado',
            'content' => 'Contenido editado',
        ]);

        $response->assertRedirect();
        $this->assertDatabaseHas('posts', [
            'id' => $post->id,
            'title' => 'Post editado',
        ]);

        // Eliminar post
        $response = $this->actingAs($user)->delete("/posts/{$post->id}");

        $response->assertRedirect('/posts');
        $this->assertDatabaseMissing('posts', ['id' => $post->id]);
    }
}

Resumen

  • Los Feature tests prueban funcionalidades completas simulando usuarios reales
  • Usa get(), post(), put(), delete() para hacer peticiones HTTP
  • Verifica códigos de estado con assertOk(), assertNotFound(), etc.
  • Comprueba vistas con assertViewIs() y assertViewHas()
  • Valida formularios con assertSessionHasErrors()
  • Usa actingAs() para simular usuarios autenticados
  • Prueba autorización verificando assertForbidden()
  • Usa Storage::fake() y UploadedFile::fake() para subida de archivos
  • Testea flujos HTTP completos para mayor confianza

Ejercicios

Ejercicio 1: Tests de un blog

Escribe Feature tests para un blog que verifiquen: la página principal muestra los últimos 10 posts, un post individual muestra título, contenido y autor, y que los posts tienen paginación.

Ver solución
<?php

namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class BlogTest extends TestCase
{
    use RefreshDatabase;

    public function test_home_page_shows_latest_10_posts(): void
    {
        // Crear 15 posts
        Post::factory()->count(15)->create();

        $response = $this->get('/');

        $response->assertOk();
        // Verificar que la vista tiene exactamente 10 posts
        $response->assertViewHas('posts', function ($posts) {
            return $posts->count() === 10;
        });
    }

    public function test_posts_are_ordered_by_newest_first(): void
    {
        $oldPost = Post::factory()->create([
            'title' => 'Post antiguo',
            'created_at' => now()->subDays(5),
        ]);

        $newPost = Post::factory()->create([
            'title' => 'Post nuevo',
            'created_at' => now(),
        ]);

        $response = $this->get('/');

        // El post nuevo debe aparecer antes
        $response->assertSeeInOrder(['Post nuevo', 'Post antiguo']);
    }

    public function test_single_post_shows_title_and_content(): void
    {
        $post = Post::factory()->create([
            'title' => 'Título del post',
            'content' => 'Contenido completo del post',
        ]);

        $response = $this->get("/posts/{$post->slug}");

        $response->assertOk();
        $response->assertSee('Título del post');
        $response->assertSee('Contenido completo del post');
    }

    public function test_single_post_shows_author_name(): void
    {
        $author = User::factory()->create(['name' => 'María López']);
        $post = Post::factory()->create(['user_id' => $author->id]);

        $response = $this->get("/posts/{$post->slug}");

        $response->assertSee('María López');
    }

    public function test_posts_have_pagination(): void
    {
        Post::factory()->count(25)->create();

        $response = $this->get('/');

        $response->assertOk();
        // Verificar que existe el enlace a página 2
        $response->assertSee('page=2');
    }

    public function test_second_page_shows_remaining_posts(): void
    {
        Post::factory()->count(15)->create();

        $response = $this->get('/?page=2');

        $response->assertOk();
        $response->assertViewHas('posts', function ($posts) {
            return $posts->count() === 5;
        });
    }
}

Ejercicio 2: Tests de comentarios

Crea tests para un sistema de comentarios donde: solo usuarios autenticados pueden comentar, los comentarios requieren contenido mínimo de 10 caracteres, y el autor del post puede eliminar cualquier comentario.

Ver solución
<?php

namespace Tests\Feature;

use App\Models\Comment;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class CommentTest extends TestCase
{
    use RefreshDatabase;

    public function test_guest_cannot_comment(): void
    {
        $post = Post::factory()->create();

        $response = $this->post("/posts/{$post->id}/comments", [
            'content' => 'Este es un comentario de prueba',
        ]);

        $response->assertRedirect('/login');
        $this->assertDatabaseCount('comments', 0);
    }

    public function test_authenticated_user_can_comment(): void
    {
        $user = User::factory()->create();
        $post = Post::factory()->create();

        $response = $this->actingAs($user)->post("/posts/{$post->id}/comments", [
            'content' => 'Este es un comentario de prueba',
        ]);

        $response->assertRedirect();
        $this->assertDatabaseHas('comments', [
            'content' => 'Este es un comentario de prueba',
            'user_id' => $user->id,
            'post_id' => $post->id,
        ]);
    }

    public function test_comment_requires_minimum_10_characters(): void
    {
        $user = User::factory()->create();
        $post = Post::factory()->create();

        $response = $this->actingAs($user)->post("/posts/{$post->id}/comments", [
            'content' => 'Corto',
        ]);

        $response->assertSessionHasErrors('content');
        $this->assertDatabaseCount('comments', 0);
    }

    public function test_comment_with_exactly_10_characters_is_valid(): void
    {
        $user = User::factory()->create();
        $post = Post::factory()->create();

        $response = $this->actingAs($user)->post("/posts/{$post->id}/comments", [
            'content' => '1234567890',
        ]);

        $response->assertSessionHasNoErrors();
        $this->assertDatabaseCount('comments', 1);
    }

    public function test_post_author_can_delete_any_comment(): void
    {
        $postAuthor = User::factory()->create();
        $commenter = User::factory()->create();

        $post = Post::factory()->create(['user_id' => $postAuthor->id]);
        $comment = Comment::factory()->create([
            'post_id' => $post->id,
            'user_id' => $commenter->id,
        ]);

        $response = $this->actingAs($postAuthor)
            ->delete("/comments/{$comment->id}");

        $response->assertRedirect();
        $this->assertDatabaseMissing('comments', ['id' => $comment->id]);
    }

    public function test_user_cannot_delete_others_comments(): void
    {
        $postAuthor = User::factory()->create();
        $commenter = User::factory()->create();
        $otherUser = User::factory()->create();

        $post = Post::factory()->create(['user_id' => $postAuthor->id]);
        $comment = Comment::factory()->create([
            'post_id' => $post->id,
            'user_id' => $commenter->id,
        ]);

        $response = $this->actingAs($otherUser)
            ->delete("/comments/{$comment->id}");

        $response->assertForbidden();
        $this->assertDatabaseHas('comments', ['id' => $comment->id]);
    }

    public function test_commenter_can_delete_own_comment(): void
    {
        $user = User::factory()->create();
        $post = Post::factory()->create();
        $comment = Comment::factory()->create([
            'post_id' => $post->id,
            'user_id' => $user->id,
        ]);

        $response = $this->actingAs($user)->delete("/comments/{$comment->id}");

        $response->assertRedirect();
        $this->assertDatabaseMissing('comments', ['id' => $comment->id]);
    }
}

Ejercicio 3: Tests de búsqueda

Escribe tests para una funcionalidad de búsqueda que verifique: la búsqueda filtra posts por título, una búsqueda vacía muestra todos los posts, y cuando no hay resultados muestra un mensaje apropiado.

Ver solución
<?php

namespace Tests\Feature;

use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class SearchTest extends TestCase
{
    use RefreshDatabase;

    public function test_search_filters_posts_by_title(): void
    {
        Post::factory()->create(['title' => 'Aprendiendo Laravel']);
        Post::factory()->create(['title' => 'Guía de PHP']);
        Post::factory()->create(['title' => 'Laravel desde cero']);

        $response = $this->get('/posts?search=Laravel');

        $response->assertOk();
        $response->assertSee('Aprendiendo Laravel');
        $response->assertSee('Laravel desde cero');
        $response->assertDontSee('Guía de PHP');
    }

    public function test_search_is_case_insensitive(): void
    {
        Post::factory()->create(['title' => 'Tutorial de LARAVEL']);

        $response = $this->get('/posts?search=laravel');

        $response->assertOk();
        $response->assertSee('Tutorial de LARAVEL');
    }

    public function test_empty_search_shows_all_posts(): void
    {
        Post::factory()->count(5)->create();

        $response = $this->get('/posts?search=');

        $response->assertOk();
        $response->assertViewHas('posts', function ($posts) {
            return $posts->count() === 5;
        });
    }

    public function test_no_search_parameter_shows_all_posts(): void
    {
        Post::factory()->count(5)->create();

        $response = $this->get('/posts');

        $response->assertOk();
        $response->assertViewHas('posts', function ($posts) {
            return $posts->count() === 5;
        });
    }

    public function test_no_results_shows_appropriate_message(): void
    {
        Post::factory()->create(['title' => 'Aprendiendo PHP']);

        $response = $this->get('/posts?search=JavaScript');

        $response->assertOk();
        $response->assertSee('No se encontraron resultados');
    }

    public function test_search_term_is_displayed_in_results(): void
    {
        Post::factory()->create(['title' => 'Tutorial Laravel']);

        $response = $this->get('/posts?search=Laravel');

        $response->assertOk();
        $response->assertSee('Resultados para: Laravel');
    }

    public function test_search_works_with_partial_matches(): void
    {
        Post::factory()->create(['title' => 'Introducción a Laravel']);

        $response = $this->get('/posts?search=Intro');

        $response->assertOk();
        $response->assertSee('Introducción a Laravel');
    }
}

¿Quieres dominar el testing en Laravel?

Aprende TDD, testing de APIs, mocking avanzado y más en nuestros cursos premium con proyectos reales.

Ver cursos premium