Skip to main content

Testing Overview

Testing ensures your API works correctly and continues to work as you make changes. This guide covers various testing approaches for the Medical Appointment Management API.

Testing with Scalar UI

The easiest way to test the API is through the built-in Scalar documentation interface.

Access Scalar

Start the API and navigate to:
https://localhost:5001/scalar/v1

Testing Workflow

1

Select an endpoint

Click on any endpoint in the left sidebar (e.g., POST /api/Paciente)
2

Fill in the request body

Enter test data in the request body editor:
{
  "nombre": "Test",
  "apellido": "Patient",
  "dni": "12345678A",
  "email": "[email protected]",
  "telefono": "+34600123456",
  "fechaNacimiento": "1990-01-01"
}
3

Send the request

Click the Send Request button
4

Inspect the response

View the response status code, headers, and body

Advantages of Scalar

  • No additional tools needed
  • Automatic documentation sync
  • Request/response schema validation
  • Code generation for multiple languages
  • Beautiful, modern interface

Testing with cURL

For command-line testing or automation, use cURL.

GET Requests

List all patients:
curl https://localhost:5001/api/Paciente
Get a specific patient:
curl https://localhost:5001/api/Paciente/1

POST Requests

Create a new specialty:
curl -X POST https://localhost:5001/api/Especialidad \
  -H "Content-Type: application/json" \
  -d '{
    "nombre": "Cardiología"
  }'
Create a new patient:
curl -X POST https://localhost:5001/api/Paciente \
  -H "Content-Type: application/json" \
  -d '{
    "nombre": "María",
    "apellido": "García",
    "dni": "87654321B",
    "email": "[email protected]",
    "telefono": "+34600111222",
    "fechaNacimiento": "1985-05-20"
  }'

PUT Requests

Update a patient:
curl -X PUT https://localhost:5001/api/Paciente/1 \
  -H "Content-Type: application/json" \
  -d '{
    "nombre": "María",
    "apellido": "García López",
    "dni": "87654321B",
    "email": "[email protected]",
    "telefono": "+34600111222",
    "fechaNacimiento": "1985-05-20"
  }'

DELETE Requests

Delete a patient:
curl -X DELETE https://localhost:5001/api/Paciente/1

Pretty-Print JSON Responses

Pipe cURL output through jq for formatted JSON:
curl https://localhost:5001/api/Paciente | jq

Testing with HTTPie

HTTPie provides a more user-friendly alternative to cURL.

Installation

# macOS
brew install httpie

# Linux
sudo apt-get install httpie

# Python (all platforms)
pip install httpie

Usage Examples

GET request:
http https://localhost:5001/api/Paciente
POST request:
http POST https://localhost:5001/api/Paciente \
  nombre="Juan" \
  apellido="Pérez" \
  dni="11223344C" \
  email="[email protected]" \
  fechaNacimiento="1992-03-10"
PUT request:
http PUT https://localhost:5001/api/Paciente/1 \
  nombre="Juan" \
  apellido="Pérez Martínez" \
  dni="11223344C" \
  email="[email protected]" \
  fechaNacimiento="1992-03-10"
DELETE request:
http DELETE https://localhost:5001/api/Paciente/1

Testing with Postman

Postman is a popular GUI tool for API testing.

Setup

  1. Download Postman from postman.com
  2. Create a new collection called “Medical Appointment API”
  3. Add the base URL as a collection variable: {{baseUrl}} = https://localhost:5001

Creating Requests

  1. Click New Request
  2. Set method (GET, POST, PUT, DELETE)
  3. Enter URL: {{baseUrl}}/api/Paciente
  4. For POST/PUT, go to BodyrawJSON and enter your data
  5. Click Send

Environment Variables

Create environments for different stages: Development:
  • baseUrl = https://localhost:5001
Production:
  • baseUrl = https://api.yourapp.com

Collection Runner

Run multiple requests in sequence:
  1. Click Runner in Postman
  2. Select your collection
  3. Click Run
This is useful for end-to-end testing scenarios.

Unit Testing Services

Test services independently without HTTP infrastructure.

Setup xUnit

dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Microsoft.NET.Test.Sdk
dotnet add package Moq

Example: Testing PacienteService

Tests/PacienteServiceTests.cs
using Xunit;
using preliminarServicios.Services;
using preliminarServicios.Models.Dtos;

namespace preliminarServicios.Tests;

public class PacienteServiceTests
{
    [Fact]
    public void ObtenerPacientes_ReturnsEmptyList_WhenNoPacientes()
    {
        // Arrange
        var service = new PacienteService();

        // Act
        var result = service.ObtenerPacientes();

        // Assert
        Assert.Empty(result);
    }

    [Fact]
    public void AgregarPaciente_CreatesPatientWithId()
    {
        // Arrange
        var service = new PacienteService();
        var dto = new CreatePacienteDto
        {
            Nombre = "Test",
            Apellido = "User",
            Dni = "12345678A",
            Email = "[email protected]",
            FechaNacimiento = new DateOnly(1990, 1, 1)
        };

        // Act
        var result = service.AgregarPaciente(dto);

        // Assert
        Assert.NotEqual(0, result.Id);
        Assert.Equal("Test", result.Nombre);
        Assert.Equal("User", result.Apellido);
    }

    [Fact]
    public void AgregarPaciente_ThrowsException_WhenDniDuplicate()
    {
        // Arrange
        var service = new PacienteService();
        var dto = new CreatePacienteDto
        {
            Nombre = "Test",
            Apellido = "User",
            Dni = "12345678A",
            Email = "[email protected]",
            FechaNacimiento = new DateOnly(1990, 1, 1)
        };
        service.AgregarPaciente(dto);

        var duplicateDto = new CreatePacienteDto
        {
            Nombre = "Another",
            Apellido = "User",
            Dni = "12345678A",
            Email = "[email protected]",
            FechaNacimiento = new DateOnly(1995, 5, 5)
        };

        // Act & Assert
        Assert.Throws<InvalidOperationException>(() => service.AgregarPaciente(duplicateDto));
    }

    [Fact]
    public void ObtenerPaciente_ThrowsKeyNotFoundException_WhenIdNotExists()
    {
        // Arrange
        var service = new PacienteService();

        // Act & Assert
        Assert.Throws<KeyNotFoundException>(() => service.ObtenerPaciente(999));
    }
}

Run Unit Tests

dotnet test
Output:
Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4

Integration Testing Controllers

Test the full HTTP request/response cycle.

Setup

dotnet add package Microsoft.AspNetCore.Mvc.Testing

Example: Testing PacienteController

Tests/PacienteControllerTests.cs
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using preliminarServicios.Models.Dtos;
using preliminarServicios.Models.Entities;
using Xunit;

namespace preliminarServicios.Tests;

public class PacienteControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public PacienteControllerTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetPacientes_ReturnsOkAndEmptyList()
    {
        // Act
        var response = await _client.GetAsync("/api/Paciente");

        // Assert
        response.EnsureSuccessStatusCode();
        var pacientes = await response.Content.ReadFromJsonAsync<List<Paciente>>();
        Assert.NotNull(pacientes);
        Assert.Empty(pacientes);
    }

    [Fact]
    public async Task CreatePaciente_ReturnsCreated()
    {
        // Arrange
        var dto = new CreatePacienteDto
        {
            Nombre = "Test",
            Apellido = "Patient",
            Dni = "99887766D",
            Email = "[email protected]",
            FechaNacimiento = new DateOnly(1988, 8, 8)
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/Paciente", dto);

        // Assert
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        var paciente = await response.Content.ReadFromJsonAsync<Paciente>();
        Assert.NotNull(paciente);
        Assert.Equal("Test", paciente.Nombre);
    }
}

Test Data Management

Creating Test Data

Create helper methods for generating test data:
Tests/TestDataFactory.cs
public static class TestDataFactory
{
    public static CreatePacienteDto CreatePacienteDto(
        string nombre = "Test",
        string apellido = "Patient",
        string dni = "12345678A",
        string email = "[email protected]")
    {
        return new CreatePacienteDto
        {
            Nombre = nombre,
            Apellido = apellido,
            Dni = dni,
            Email = email,
            FechaNacimiento = new DateOnly(1990, 1, 1)
        };
    }

    public static CreateMedicoDto CreateMedicoDto(int especialidadId = 1)
    {
        return new CreateMedicoDto
        {
            Nombre = "Dr. Test",
            Apellido = "Doctor",
            NumeroLicencia = "LIC-TEST-001",
            EspecialidadId = especialidadId
        };
    }
}
Use in tests:
[Fact]
public void Test_WithTestData()
{
    var pacienteDto = TestDataFactory.CreatePacienteDto(dni: "11111111A");
    var result = _service.AgregarPaciente(pacienteDto);
    // ...
}

End-to-End Testing Scenarios

Scenario: Complete Appointment Booking

Complete appointment workflow
#!/bin/bash

BASE_URL="https://localhost:5001"

# 1. Create specialty
echo "Creating specialty..."
ESPECIALIDAD=$(curl -s -X POST "$BASE_URL/api/Especialidad" \
  -H "Content-Type: application/json" \
  -d '{"nombre": "Medicina General"}')
ESPECIALIDAD_ID=$(echo $ESPECIALIDAD | jq -r '.id')
echo "Created specialty with ID: $ESPECIALIDAD_ID"

# 2. Create doctor
echo "Creating doctor..."
MEDICO=$(curl -s -X POST "$BASE_URL/api/Medico" \
  -H "Content-Type: application/json" \
  -d "{
    \"nombre\": \"Dr. Juan\",
    \"apellido\": \"Pérez\",
    \"numeroLicencia\": \"MED-001\",
    \"especialidadId\": $ESPECIALIDAD_ID
  }")
MEDICO_ID=$(echo $MEDICO | jq -r '.id')
echo "Created doctor with ID: $MEDICO_ID"

# 3. Create patient
echo "Creating patient..."
PACIENTE=$(curl -s -X POST "$BASE_URL/api/Paciente" \
  -H "Content-Type: application/json" \
  -d '{
    "nombre": "María",
    "apellido": "García",
    "dni": "12345678A",
    "email": "[email protected]",
    "fechaNacimiento": "1985-03-15"
  }')
PACIENTE_ID=$(echo $PACIENTE | jq -r '.id')
echo "Created patient with ID: $PACIENTE_ID"

# 4. Create appointment
echo "Creating appointment..."
CITA=$(curl -s -X POST "$BASE_URL/api/Cita" \
  -H "Content-Type: application/json" \
  -d "{
    \"pacienteId\": $PACIENTE_ID,
    \"medicoId\": $MEDICO_ID,
    \"costo\": 50.00,
    \"motivo\": \"Consulta general\",
    \"fechaInicio\": \"2024-04-01T10:00:00\",
    \"fechaFin\": \"2024-04-01T10:30:00\",
    \"estado\": 0
  }")
CITA_ID=$(echo $CITA | jq -r '.id')
echo "Created appointment with ID: $CITA_ID"

# 5. Verify appointment
echo "Verifying appointment..."
curl -s "$BASE_URL/api/Cita/$CITA_ID" | jq

echo "End-to-end test completed successfully!"

Performance Testing

Using Apache Bench

Test API performance:
# Install Apache Bench (comes with Apache HTTP Server)
# macOS: included
# Linux: sudo apt-get install apache2-utils

# Test GET endpoint with 1000 requests, 10 concurrent
ab -n 1000 -c 10 https://localhost:5001/api/Paciente

Using wrk

# Install wrk
brew install wrk  # macOS
sudo apt-get install wrk  # Linux

# Run load test
wrk -t4 -c100 -d30s https://localhost:5001/api/Paciente

Test Coverage

Generate Code Coverage Report

# Install coverage tool
dotnet add package coverlet.collector

# Run tests with coverage
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover

# Generate HTML report
reportgenerator -reports:coverage.opencover.xml -targetdir:coverage-report

Best Practices

Test Business Logic

Focus unit tests on service layer where business logic lives

Use Integration Tests

Test full HTTP flow for critical user journeys

Arrange-Act-Assert

Structure tests clearly: setup, execute, verify

Meaningful Test Names

Use descriptive names: Method_Scenario_ExpectedResult

Test Edge Cases

Test null values, empty lists, invalid input

Isolate Tests

Each test should be independent and repeatable

Common Test Scenarios

Validation Testing

[Theory]
[InlineData("", "Apellido", "12345678A", "[email protected]")] // Empty nombre
[InlineData("Nombre", "", "12345678A", "[email protected]")] // Empty apellido
[InlineData("Nombre", "Apellido", "123", "[email protected]")] // Invalid DNI
[InlineData("Nombre", "Apellido", "12345678A", "invalid-email")] // Invalid email
public void AgregarPaciente_ThrowsException_WithInvalidData(
    string nombre, string apellido, string dni, string email)
{
    var service = new PacienteService();
    var dto = new CreatePacienteDto
    {
        Nombre = nombre,
        Apellido = apellido,
        Dni = dni,
        Email = email,
        FechaNacimiento = new DateOnly(1990, 1, 1)
    };

    Assert.Throws<ValidationException>(() => service.AgregarPaciente(dto));
}

Conflict Detection Testing

[Fact]
public void AgregarCita_ThrowsException_WhenDoctorHasConflict()
{
    var service = new CitaService(_pacienteService, _medicoService);
    
    // Create first appointment
    var dto1 = new CreateCitaDto
    {
        PacienteId = 1,
        MedicoId = 1,
        Costo = 50,
        Motivo = "Consulta",
        FechaInicio = new DateTime(2024, 4, 1, 10, 0, 0),
        FechaFin = new DateTime(2024, 4, 1, 10, 30, 0),
        Estado = CitaEstado.Pendiente
    };
    service.AgregarCita(dto1);
    
    // Try to create overlapping appointment
    var dto2 = new CreateCitaDto
    {
        PacienteId = 2,
        MedicoId = 1, // Same doctor
        Costo = 50,
        Motivo = "Consulta",
        FechaInicio = new DateTime(2024, 4, 1, 10, 15, 0), // Overlaps
        FechaFin = new DateTime(2024, 4, 1, 10, 45, 0),
        Estado = CitaEstado.Pendiente
    };
    
    Assert.Throws<InvalidOperationException>(() => service.AgregarCita(dto2));
}

Continuous Integration

Add testing to your CI/CD pipeline:
.github/workflows/test.yml
name: Run Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '10.0.x'
      - name: Restore dependencies
        run: dotnet restore
      - name: Build
        run: dotnet build --no-restore
      - name: Test
        run: dotnet test --no-build --verbosity normal

Next Steps

Development Guide

Learn how to extend the API

Architecture

Understand the system design

API Reference

Explore all endpoints

Services

Deep dive into service patterns

Build docs developers (and LLMs) love