Módulo 16: Proyecto Final — Plan de Pruebas Completo

Objetivo

Aplicar todos los conocimientos del curso en un proyecto integrador: diseñar y ejecutar un plan de pruebas completo para una aplicación web de gestión de tareas (Todo App).


1. Descripción de la aplicación

Todo App — Gestor de Tareas

Una API REST con frontend web que permite:

Funcionalidad Endpoint API UI
Registrar usuario POST /api/auth/register Formulario registro
Iniciar sesión POST /api/auth/login Formulario login
Listar tareas GET /api/tareas Tabla con tareas
Crear tarea POST /api/tareas Formulario nueva tarea
Editar tarea PUT /api/tareas/{id} Modal de edición
Eliminar tarea DELETE /api/tareas/{id} Botón eliminar
Marcar completada PATCH /api/tareas/{id}/completar Checkbox
Filtrar por estado GET /api/tareas?estado=pendiente Dropdown filtro

Modelo de datos

public class Tarea
{
    public int Id { get; set; }
    public string Titulo { get; set; } = "";
    public string? Descripcion { get; set; }
    public Prioridad Prioridad { get; set; }     // Alta, Media, Baja
    public EstadoTarea Estado { get; set; }       // Pendiente, EnProgreso, Completada
    public DateTime FechaCreacion { get; set; }
    public DateTime? FechaLimite { get; set; }
    public int UsuarioId { get; set; }
}

public enum Prioridad { Baja, Media, Alta }
public enum EstadoTarea { Pendiente, EnProgreso, Completada }

2. Plan de pruebas

2.1 Alcance

Incluido Excluido
API REST completa Rendimiento de base de datos
Frontend web (Chrome, Firefox) Aplicación móvil
Autenticación JWT Pruebas de seguridad avanzadas
CRUD de tareas Integración con terceros

2.2 Tipos de pruebas

graph TD
    A[Plan de Pruebas] --> B[Pruebas Unitarias]
    A --> C[Pruebas de API]
    A --> D[Pruebas de UI]
    A --> E[Pruebas de Rendimiento]
    B --> B1[Servicios]
    B --> B2[Validaciones]
    C --> C1[Postman/Newman]
    D --> D1[Playwright]
    E --> E1[JMeter]
    style A fill:#2196F3,color:white
    style B fill:#4CAF50,color:white
    style C fill:#FF9800,color:white
    style D fill:#9C27B0,color:white
    style E fill:#f44336,color:white

2.3 Entornos

Entorno URL Base de datos
Desarrollo http://localhost:5000 SQLite
CI/CD GitHub Actions runner SQLite en memoria
Staging https://staging.todoapp.com SQL Server

3. Pruebas Unitarias (NUnit)

3.1 Estructura del proyecto de tests

TodoApp.Tests/
├── Unit/
│   ├── Services/
│   │   ├── TareaServiceTests.cs
│   │   └── AuthServiceTests.cs
│   └── Validators/
│       └── TareaValidatorTests.cs
├── Integration/
│   └── TareaRepositoryTests.cs
└── TodoApp.Tests.csproj

3.2 Tests del servicio de tareas

using NSubstitute;

namespace TodoApp.Tests.Unit.Services;

[TestFixture]
public class TareaServiceTests
{
    private ITareaRepository _repo;
    private TareaService _service;

    [SetUp]
    public void Setup()
    {
        _repo = Substitute.For<ITareaRepository>();
        _service = new TareaService(_repo);
    }

    // --- CREAR TAREA ---

    [Test]
    public async Task CrearTarea_DatosValidos_RetornaId()
    {
        // Arrange
        var nueva = new CrearTareaDto
        {
            Titulo = "Comprar leche",
            Prioridad = Prioridad.Media
        };
        _repo.CrearAsync(Arg.Any<Tarea>()).Returns(1);

        // Act
        var id = await _service.CrearAsync(nueva, usuarioId: 1);

        // Assert
        Assert.That(id, Is.EqualTo(1));
        await _repo.Received(1).CrearAsync(Arg.Is<Tarea>(
            t => t.Titulo == "Comprar leche" &&
                 t.Estado == EstadoTarea.Pendiente));
    }

    [Test]
    public void CrearTarea_TituloVacio_LanzaExcepcion()
    {
        var nueva = new CrearTareaDto { Titulo = "" };
        Assert.ThrowsAsync<ValidationException>(
            () => _service.CrearAsync(nueva, usuarioId: 1));
    }

    [Test]
    public void CrearTarea_TituloMuyLargo_LanzaExcepcion()
    {
        var nueva = new CrearTareaDto { Titulo = new string('A', 201) };
        Assert.ThrowsAsync<ValidationException>(
            () => _service.CrearAsync(nueva, usuarioId: 1));
    }

    // --- LISTAR TAREAS ---

    [Test]
    public async Task ListarTareas_UsuarioConTareas_RetornaLista()
    {
        _repo.ObtenerPorUsuarioAsync(1).Returns(new List<Tarea>
        {
            new() { Id = 1, Titulo = "Tarea 1", UsuarioId = 1 },
            new() { Id = 2, Titulo = "Tarea 2", UsuarioId = 1 }
        });

        var resultado = await _service.ListarAsync(usuarioId: 1);

        Assert.That(resultado, Has.Count.EqualTo(2));
    }

    [Test]
    public async Task ListarTareas_UsuarioSinTareas_RetornaVacio()
    {
        _repo.ObtenerPorUsuarioAsync(99).Returns(new List<Tarea>());
        var resultado = await _service.ListarAsync(usuarioId: 99);
        Assert.That(resultado, Is.Empty);
    }

    // --- COMPLETAR TAREA ---

    [Test]
    public async Task CompletarTarea_TareaPendiente_CambiaEstado()
    {
        var tarea = new Tarea
        {
            Id = 1, Titulo = "Test",
            Estado = EstadoTarea.Pendiente, UsuarioId = 1
        };
        _repo.ObtenerPorIdAsync(1).Returns(tarea);

        await _service.CompletarAsync(1, usuarioId: 1);

        await _repo.Received(1).ActualizarAsync(Arg.Is<Tarea>(
            t => t.Estado == EstadoTarea.Completada));
    }

    [Test]
    public void CompletarTarea_TareaNoExiste_LanzaNotFound()
    {
        _repo.ObtenerPorIdAsync(999).Returns((Tarea?)null);
        Assert.ThrowsAsync<NotFoundException>(
            () => _service.CompletarAsync(999, usuarioId: 1));
    }

    [Test]
    public void CompletarTarea_TareaDeOtroUsuario_LanzaForbidden()
    {
        var tarea = new Tarea { Id = 1, UsuarioId = 2 };
        _repo.ObtenerPorIdAsync(1).Returns(tarea);
        Assert.ThrowsAsync<ForbiddenException>(
            () => _service.CompletarAsync(1, usuarioId: 1));
    }

    // --- ELIMINAR TAREA ---

    [Test]
    public async Task EliminarTarea_TareaPropia_Elimina()
    {
        var tarea = new Tarea { Id = 1, UsuarioId = 1 };
        _repo.ObtenerPorIdAsync(1).Returns(tarea);

        await _service.EliminarAsync(1, usuarioId: 1);

        await _repo.Received(1).EliminarAsync(1);
    }
}

3.3 Tests de validación

[TestFixture]
public class TareaValidatorTests
{
    [TestCase("Comprar leche", true)]
    [TestCase("", false)]
    [TestCase(null, false)]
    [TestCase("AB", false)]             // Menos de 3 caracteres
    public void ValidarTitulo_CasosVarios(string? titulo, bool esperado)
    {
        var resultado = TareaValidator.ValidarTitulo(titulo);
        Assert.That(resultado, Is.EqualTo(esperado));
    }

    [Test]
    public void ValidarFechaLimite_FechaPasada_RetornaFalse()
    {
        var resultado = TareaValidator.ValidarFechaLimite(
            DateTime.Now.AddDays(-1));
        Assert.That(resultado, Is.False);
    }

    [Test]
    public void ValidarFechaLimite_FechaFutura_RetornaTrue()
    {
        var resultado = TareaValidator.ValidarFechaLimite(
            DateTime.Now.AddDays(7));
        Assert.That(resultado, Is.True);
    }
}

4. Pruebas de API (Postman/Newman)

4.1 Colección Postman

TodoApp API Tests/
├── Auth/
│   ├── POST Register - Registro exitoso
│   ├── POST Register - Email duplicado → 409
│   ├── POST Login - Credenciales válidas → token
│   └── POST Login - Credenciales inválidas → 401
├── Tareas/
│   ├── POST Crear tarea → 201
│   ├── POST Crear tarea sin título → 400
│   ├── GET Listar tareas → 200
│   ├── GET Listar tareas sin auth → 401
│   ├── PUT Editar tarea → 200
│   ├── PUT Editar tarea de otro usuario → 403
│   ├── PATCH Completar tarea → 200
│   ├── DELETE Eliminar tarea → 204
│   └── DELETE Eliminar tarea inexistente → 404
└── Filtros/
    ├── GET Tareas pendientes → filtra correctamente
    ├── GET Tareas completadas → filtra correctamente
    └── GET Tareas por prioridad → filtra correctamente

4.2 Tests de ejemplo en Postman

// POST /api/auth/login — Tests
pm.test("Status 200", () => pm.response.to.have.status(200));

pm.test("Retorna token", () => {
    const json = pm.response.json();
    pm.expect(json.token).to.be.a("string");
    pm.expect(json.token.length).to.be.greaterThan(0);
    pm.collectionVariables.set("token", json.token);
});

// POST /api/tareas — Tests
pm.test("Status 201 Created", () => pm.response.to.have.status(201));

pm.test("Retorna tarea con id", () => {
    const json = pm.response.json();
    pm.expect(json.id).to.be.a("number");
    pm.expect(json.titulo).to.eql("Comprar leche");
    pm.expect(json.estado).to.eql("Pendiente");
    pm.collectionVariables.set("tareaId", json.id);
});

// GET /api/tareas sin auth — Tests
pm.test("Status 401 Unauthorized", () => pm.response.to.have.status(401));

4.3 Ejecución con Newman

newman run TodoApp.postman_collection.json \
  -e entorno-desarrollo.json \
  --reporters cli,htmlextra \
  --reporter-htmlextra-export reports/api-report.html

5. Pruebas de UI (Playwright)

5.1 Page Objects

// Pages/LoginPage.cs
public class LoginPage
{
    private readonly IPage _page;

    public LoginPage(IPage page) => _page = page;

    private ILocator EmailInput => _page.GetByLabel("Email");
    private ILocator PasswordInput => _page.GetByLabel("Contraseña");
    private ILocator LoginButton => _page.GetByRole(AriaRole.Button,
        new() { Name = "Iniciar sesión" });
    private ILocator ErrorAlert => _page.Locator(".alert-danger");

    public async Task GotoAsync() => await _page.GotoAsync("/login");

    public async Task LoginAsync(string email, string password)
    {
        await EmailInput.FillAsync(email);
        await PasswordInput.FillAsync(password);
        await LoginButton.ClickAsync();
    }

    public ILocator GetError() => ErrorAlert;
}

// Pages/TareasPage.cs
public class TareasPage
{
    private readonly IPage _page;

    public TareasPage(IPage page) => _page = page;

    private ILocator NuevaTareaBtn => _page.GetByRole(AriaRole.Button,
        new() { Name = "Nueva tarea" });
    private ILocator TituloInput => _page.GetByLabel("Título");
    private ILocator PrioridadSelect => _page.GetByLabel("Prioridad");
    private ILocator GuardarBtn => _page.GetByRole(AriaRole.Button,
        new() { Name = "Guardar" });
    private ILocator TareasLista => _page.Locator("[data-testid='tarea-item']");
    private ILocator FiltroEstado => _page.GetByLabel("Filtrar por estado");

    public async Task CrearTareaAsync(string titulo, string prioridad)
    {
        await NuevaTareaBtn.ClickAsync();
        await TituloInput.FillAsync(titulo);
        await PrioridadSelect.SelectOptionAsync(prioridad);
        await GuardarBtn.ClickAsync();
    }

    public ILocator GetTareas() => TareasLista;

    public async Task FiltrarPorEstadoAsync(string estado)
    {
        await FiltroEstado.SelectOptionAsync(estado);
    }

    public async Task CompletarTareaAsync(string titulo)
    {
        var tarea = TareasLista.Filter(new() { HasText = titulo });
        await tarea.GetByRole(AriaRole.Checkbox).CheckAsync();
    }

    public async Task EliminarTareaAsync(string titulo)
    {
        var tarea = TareasLista.Filter(new() { HasText = titulo });
        await tarea.GetByRole(AriaRole.Button,
            new() { Name = "Eliminar" }).ClickAsync();
    }
}

5.2 Tests UI

[TestFixture]
public class TareasUITests : PageTest
{
    private LoginPage _loginPage = null!;
    private TareasPage _tareasPage = null!;

    [SetUp]
    public async Task Setup()
    {
        _loginPage = new LoginPage(Page);
        _tareasPage = new TareasPage(Page);

        await _loginPage.GotoAsync();
        await _loginPage.LoginAsync("test@test.com", "Pass123!");
    }

    [Test]
    public async Task CrearTarea_DatosValidos_ApareceEnLista()
    {
        await _tareasPage.CrearTareaAsync("Estudiar testing", "Alta");

        await Expect(_tareasPage.GetTareas()).ToContainTextAsync(
            "Estudiar testing");
    }

    [Test]
    public async Task CompletarTarea_MarcaCheckbox_CambiaEstado()
    {
        await _tareasPage.CompletarTareaAsync("Estudiar testing");

        await _tareasPage.FiltrarPorEstadoAsync("Completada");
        await Expect(_tareasPage.GetTareas()).ToContainTextAsync(
            "Estudiar testing");
    }

    [Test]
    public async Task EliminarTarea_ConfirmaEliminar_DesapareceDeLista()
    {
        await _tareasPage.EliminarTareaAsync("Estudiar testing");

        await Expect(_tareasPage.GetTareas()).Not.ToContainTextAsync(
            "Estudiar testing");
    }

    [Test]
    public async Task FiltrarTareas_PorPendientes_MuestraSoloPendientes()
    {
        await _tareasPage.FiltrarPorEstadoAsync("Pendiente");

        var tareas = _tareasPage.GetTareas();
        var count = await tareas.CountAsync();

        for (int i = 0; i < count; i++)
        {
            await Expect(tareas.Nth(i).Locator(".estado"))
                .ToHaveTextAsync("Pendiente");
        }
    }
}

6. Pruebas de Rendimiento (JMeter)

6.1 Escenarios

Escenario Usuarios Ramp-up Duración
Carga normal 50 10s 60s
Carga alta 200 30s 120s
Stress 500 60s 180s

6.2 Plan de rendimiento

Test Plan: TodoApp Performance
├── Thread Group: Carga Normal (50 users, 10s ramp-up, 60s)
│   ├── POST /api/auth/login → JSON Extractor ${token}
│   ├── HTTP Header Manager (Bearer ${token})
│   ├── GET /api/tareas
│   │   Response Assertion: 200
│   │   Duration Assertion: < 2000ms
│   ├── POST /api/tareas
│   │   Response Assertion: 201
│   ├── Constant Timer: 1000ms
│   └── GET /api/tareas?estado=pendiente
│       Duration Assertion: < 1500ms
├── Summary Report
└── Aggregate Report

6.3 Criterios de aceptación

Métrica Carga normal Carga alta Stress
Tiempo medio < 500ms < 1000ms < 3000ms
p90 < 1000ms < 2000ms < 5000ms
Error rate < 1% < 2% < 10%
Throughput > 50 req/s > 30 req/s > 10 req/s

7. Pipeline CI/CD

name: TodoApp - CI/CD Completo

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'
      - run: dotnet test TodoApp.Tests --filter "Category=Unit"
            --collect:"XPlat Code Coverage"
      - uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: "**/coverage.cobertura.xml"

  api-tests:
    runs-on: ubuntu-latest
    needs: unit-tests
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm install -g newman newman-reporter-htmlextra
      - run: newman run tests/TodoApp.postman_collection.json
              -e tests/env-ci.json
              --reporters cli,htmlextra
              --reporter-htmlextra-export reports/api.html
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: api-report
          path: reports/

  ui-tests:
    runs-on: ubuntu-latest
    needs: unit-tests
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'
      - run: dotnet build TodoApp.PlaywrightTests
      - run: pwsh TodoApp.PlaywrightTests/bin/Debug/net9.0/playwright.ps1
              install --with-deps
      - run: dotnet test TodoApp.PlaywrightTests
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-traces
          path: "**/traces/"

8. Matriz de trazabilidad

Requisito Unit Test API Test UI Test Performance
Registro de usuario AuthServiceTests POST Register - -
Login AuthServiceTests POST Login LoginTests Login JMeter
Crear tarea TareaServiceTests POST Tareas CrearTareaTest POST Tareas JMeter
Listar tareas TareaServiceTests GET Tareas - GET Tareas JMeter
Completar tarea TareaServiceTests PATCH Completar CompletarTest -
Eliminar tarea TareaServiceTests DELETE Tareas EliminarTest -
Filtrar tareas TareaServiceTests GET Tareas?estado FiltrarTest GET Filtro JMeter

9. Rúbrica de evaluación

Criterio Peso Descripción
Plan de pruebas 10% Documento con alcance, tipos, entornos
Tests unitarios 25% Mínimo 15 tests con mocks, cobertura > 80%
Tests API (Postman) 20% Colección con tests para todos los endpoints
Tests UI (Playwright) 20% Page Objects + tests para flujos principales
Performance (JMeter) 10% Plan de rendimiento con assertions
CI/CD (GitHub Actions) 10% Pipeline funcional con todos los tipos
Reportes y métricas 5% Coverage report, Allure o similar

10. Entrega

El proyecto debe incluir:

TodoApp-Testing/
├── docs/
│   └── plan-de-pruebas.md          ← Documento del plan
├── TodoApp.Tests/
│   ├── Unit/                        ← Tests unitarios
│   └── Integration/                 ← Tests de integración
├── TodoApp.PlaywrightTests/
│   ├── Pages/                       ← Page Objects
│   └── Tests/                       ← Tests UI
├── tests/
│   ├── TodoApp.postman_collection.json  ← Colección Postman
│   └── env-ci.json                  ← Entorno CI
├── jmeter/
│   └── TodoApp-LoadTest.jmx         ← Plan JMeter
├── .github/
│   └── workflows/
│       └── ci.yml                   ← Pipeline CI/CD
└── README.md                        ← Instrucciones

¡Enhorabuena! Has completado el Curso de Testing. Ahora tienes las herramientas y conocimientos para diseñar, implementar y automatizar pruebas profesionales en cualquier proyecto.