SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

Mocking y pruebas de API autoalojadas en un VPS

Ejecuta WireMock y Hurl en tu VPS: stubs versionados en Git, suites en CI y reportes persistentes incluso después de reconstruir el servidor.

Dos trabajos que comparten un repositorio

El mocking y las pruebas de API autoalojados son dos trabajos diferentes, y tratarlos como uno solo hace perder una semana. Un mock server sustituye a una dependencia que no se puede invocar desde CI: un proveedor de pagos, una API de un socio, un upstream con límites de tasa o un servicio que otro equipo todavía no ha publicado. Un API test runner llama a sus propios endpoints en un orden fijo y comprueba las respuestas. También transfiere valores de una respuesta a la siguiente solicitud.

No se solapan. Un mock server nunca informa de si una prueba pasa o falla. Un test runner no determina qué devuelve un proveedor de pagos cuando se rechaza una tarjeta. La mayoría de los equipos que ya alquilan un servidor terminan ejecutando uno de cada tipo, iniciados por el mismo archivo de Docker Compose y revisados en el mismo pull request.

¿Por qué alojar uno mismo el mocking y las pruebas de API?

Los fixtures tienen una estructura similar a la de los datos de producción. El cuerpo de una petición en una prueba de API puede ser un registro real de cliente con el nombre cambiado, o con el nombre sin cambiar porque nadie lo comprobó. Los stubs grabados son peores: la grabación mediante proxy almacena lo que devolvió realmente el upstream, por lo que un directorio de stubs creado mediante grabación contiene tokens activos y direcciones de correo de clientes hasta que alguien lee todos los archivos. En un servicio alojado, esos datos se convierten en un incidente y una divulgación de otra organización.

La segunda razón es la accesibilidad. Un servicio vinculado a una dirección privada no es accesible desde un runner alojado, por lo que la prueba no puede ejecutarse. Cada alternativa tiene un coste. Publicar la API en Internet para probarla elimina el motivo por el que era privada. Un túnel o una copia pública de staging es otro sistema que hay que mantener, y una copia de staging se desvía de producción entre versiones. Un runner en la misma red privada llama directamente al servicio y no necesita nada de eso. Ese es el argumento práctico a favor de un runner de GitHub Actions autohospedado.

¿Qué servidor mock autohospedado debería ejecutar?

Cada uno se ejecuta como un contenedor en un servidor que usted controla. La cuestión importante es qué considera cada uno como fuente de verdad, porque eso determina si reconstruir el contenedor no tiene ningún coste o le lleva toda una tarde.

  • WireMock guarda cada stub como un archivo JSON en un directorio mappings/, con los cuerpos de respuesta grandes en __files/. La imagen es wiremock/wiremock, su directorio raíz dentro del contenedor es /home/wiremock y también funciona como proxy de grabación. Los archivos en disco permiten mantener el mock en git como cualquier otro código.
  • Mockoon CLI guarda una API mock completa en un único archivo de datos JSON. Instálelo con npm install -g @mockoon/cli e inícielo con mockoon-cli start --data ./data-file.json, o ejecute la imagen mockoon/cli con ese archivo montado mediante un bind mount. La aplicación de escritorio edita el mismo archivo, por lo que diseñar mediante una interfaz gráfica y confirmar el resultado en el repositorio siguen siendo compatibles.
  • MockServer se ejecuta desde la imagen mockserver/mockserver y escucha en el puerto 1080. Las expectativas llegan mediante su propia API REST, lo que resulta práctico desde el código de pruebas, pero arriesgado para un despliegue: una expectativa creada mediante una llamada HTTP desaparece cuando se reinicia el contenedor. Use su archivo de inicialización JSON para los stubs que deban ser permanentes.
  • Prism construye el mock a partir del documento OpenAPI, en lugar de usar archivos de stubs independientes. Instálelo con npm install -g @stoplight/prism-cli y, después, ejecútelo con prism mock openapi.yaml. Dentro de un contenedor, añada -h 0.0.0.0, porque Prism se enlaza a localhost de forma predeterminada y, de lo contrario, no se puede acceder a él desde fuera del contenedor.
  • Microcks es la opción más completa: proporciona una interfaz web que importa documentos OpenAPI y colecciones de Postman, los sirve como mocks y ejecuta pruebas de contrato. Una instalación completa necesita MongoDB y Keycloak, además de Kafka para sus funciones asíncronas. La imagen todo en uno microcks-uber incluye un MongoDB en memoria, que el proyecto documenta como adecuado para usos efímeros. Por tanto, trate como desechable todo lo que cree en esa interfaz y mantenga los artefactos de origen en git.

¿Qué ejecutor de pruebas de API autoalojado debería usar?

El trabajo consiste en una secuencia: autenticarse, crear un pedido, volver a leerlo y comprobar que el estado ha cambiado. Para ello, hay que capturar un valor de una respuesta y usarlo en la siguiente petición. Una herramienta que no puede conservar el estado entre llamadas sirve para comprobar el estado, no para probar una API.

  • Hurl ejecuta archivos de texto plano con peticiones HTTP desde un único binario. Una sección [Captures] extrae valores de una respuesta, una sección [Asserts] los comprueba y --test lo convierte en un ejecutor de pruebas con un resumen y un código de salida. La versión 8.0.1 es la actual en agosto de 2026.
  • Bruno CLI ejecuta una carpeta de archivos .bru. Se instala con npm install -g @usebruno/cli y se ejecuta con bru run folder --env Local --reporter-junit results.xml. El formato de la colección consiste por diseño en archivos de texto dentro de un directorio, por lo que las diferencias son fáciles de revisar.
  • Newman ejecuta colecciones de Postman fuera de Postman: npm install -g newman y después newman run collection.json -r cli,junit --reporter-junit-export results.xml. El inconveniente es el formato. La colección es un único bloque JSON exportado, por lo que la edición se realiza en Postman y el archivo del repositorio es una copia que queda obsoleta.
  • Schemathesis comprueba algo diferente. Lee un esquema OpenAPI y genera casos que intentan producir respuestas que el esquema considera imposibles: uvx schemathesis run https://your.api/openapi.json. Detecta fallos y violaciones del contrato, pero no conoce las reglas de negocio, por lo que debe usarse junto a una suite con pruebas programadas, no como sustituto.
  • Hoppscotch autoalojado es la opción con interfaz web y requiere una instancia de Postgres. Tenga en cuenta esta decisión antes de instalarlo: las colecciones se almacenan en una base de datos, no en el repositorio.

Hay una opción que conviene evitar. Step CI todavía aparece en recopilaciones de herramientas y su formato de flujo de trabajo YAML es fácil de leer, pero el repositorio recibió su último commit en agosto de 2024. Un programa que se sitúa entre el sistema de CI y la API es un lugar inadecuado para usar código sin mantenimiento.

Coloque el servidor simulado detrás del firewall

La configuración siguiente ejecuta WireMock como sustituto de un proveedor de pagos. Si el formato del archivo de Compose es nuevo para usted, Docker Compose en un VPS explica los comandos del ciclo de vida que se utilizan en esta sección.

services:
  mock-payments:
    image: wiremock/wiremock:3.13.2
    command: ["--verbose"]
    volumes:
      - ./mocks/payments:/home/wiremock
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped

El prefijo 127.0.0.1: del puerto es la parte importante. Un 8080:8080 sin dirección publica el servidor simulado en todas las interfaces, incluida la IP pública, y sigue siendo accesible aunque ufw deniegue ese puerto, porque Docker escribe sus propias reglas en la cadena DOCKER de iptables y estas se evalúan antes que las reglas INPUT de ufw. En su lugar, vincule el servicio a la dirección de loopback o a la dirección de una interfaz privada. Así, el kernel nunca acepta desde el exterior esa conexión.

El servicio que se está probando debe apuntar entonces al servidor simulado. Cuando el servicio se ejecuta en el mismo proyecto de Compose, la URL base del servidor simulado es http://mock-payments:8080, porque Compose resuelve los nombres de servicio en su propia red. Cuando el servicio se ejecuta en el host, es http://127.0.0.1:8080. Configure este valor mediante una variable de entorno, nunca en el código, o la URL de prueba se distribuirá a producción.

Los stubs se guardan en ./mocks/payments/mappings/, con un archivo JSON para cada uno.

{
  "request": {
    "method": "POST",
    "urlPath": "/v1/charges",
    "bodyPatterns": [{ "matchesJsonPath": "$.amount" }]
  },
  "response": {
    "status": 201,
    "headers": { "Content-Type": "application/json" },
    "jsonBody": { "id": "ch_test_001", "status": "succeeded", "amount": 4200 }
  }
}

Inícielo y compruebe qué se cargó realmente.

docker compose up -d --wait mock-payments
curl -fsS http://127.0.0.1:8080/__admin/mappings

--wait espera hasta que el contenedor indique que está saludable. Esto funciona porque la imagen de WireMock incluye un HEALTHCHECK para su endpoint /__admin/health. La llamada mappings muestra todos los stubs que el servidor leyó. Si un stub que escribió no aparece en esa lista, nunca se cargó: compruebe que el archivo esté bajo mappings/ y no en la raíz montada, y verifique que el JSON se pueda analizar.

Cuando llega una petición y ningún stub coincide, WireMock responde con 404 y un cuerpo que comienza por Request was not matched, seguido de una diferencia respecto al stub más parecido que tiene almacenado. Lea esa diferencia antes de cambiar nada, porque indica el campo exacto que no coincide. Normalmente es una ruta con /v1/charge donde el stub contiene /v1/charges.

Escriba la prueba como una secuencia con el estado entre llamadas

Los archivos de Hurl son archivos de texto sin formato. Instale el paquete deb desde las versiones publicadas del proyecto.

VERSION=8.0.1
curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VERSION/hurl_${VERSION}_amd64.deb
sudo apt update && sudo apt install ./hurl_${VERSION}_amd64.deb

Una suite que prueba su propia API contra el mock se encuentra en tests/checkout.hurl.

POST {{base_url}}/orders
Content-Type: application/json
{
  "sku": "ssd-1tb",
  "amount": 4200
}
HTTP 201
[Captures]
order_id: jsonpath "$['id']"

GET {{base_url}}/orders/{{order_id}}
HTTP 200
[Asserts]
jsonpath "$.status" == "paid"
jsonpath "$.charge_id" == "ch_test_001"

El bloque [Captures] es lo que convierte esto en una prueba de API, en lugar de dos solicitudes sin relación. order_id se lee de la primera respuesta y se interpola en la URL de la segunda. La aserción sobre charge_id es el objetivo de todo el ejercicio: demuestra que el servicio llamó al proveedor de pagos y almacenó la respuesta recibida. El valor con el que se compara es el que escribió en el stub de WireMock. Un solo archivo cubre ahora las dos partes del flujo.

hurl --test --variable base_url=http://127.0.0.1:3000 \
  --report-junit reports/junit.xml \
  --report-json reports/json \
  tests/

Una ejecución correcta imprime una línea por archivo y un resumen.

tests/checkout.hurl: Success (2 request(s) in 61 ms)
Executed files:    1
Executed requests: 2 (30.1/s)
Succeeded files:   1 (100.0%)
Failed files:      0 (0.0%)
Duration:          64 ms

Una ejecución fallida imprime error: Assert failure con el archivo y el número de línea. Después muestra el valor obtenido junto al valor esperado, y hurl termina con un código distinto de cero para que CI se detenga. Si status lee pending cuando esperaba paid, el servicio no procesó la respuesta del mock. Lo siguiente que debe revisar es el registro de solicitudes de WireMock en /__admin/requests. Este muestra si la llamada llegó al mock.

Ejecute la suite desde su propio runner de CI

Con un runner registrado en el mismo equipo, el flujo es breve. El runner es un proceso normal del host, por lo que docker y hurl deben estar instalados en ese host. No se hereda nada de una imagen alojada.

name: api-tests
on: [push]
jobs:
  hurl:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - name: Start the mock
        run: docker compose up -d --wait mock-payments
      - name: Run the suite
        run: hurl --test --variable base_url=http://127.0.0.1:3000 --report-junit reports/junit.xml tests/
      - name: Archive the reports
        if: always()
        run: install -d /srv/api-tests/reports/$GITHUB_SHA && cp -r reports/. /srv/api-tests/reports/$GITHUB_SHA/
      - name: Stop the mock
        if: always()
        run: docker compose down

if: always() en el paso de archivado es importante. Sin esta opción, una ejecución de pruebas fallida omite la copia y se pierde precisamente el informe que se quería revisar. La copia también debe quedar fuera del workspace, porque el runner limpia el workspace antes del siguiente trabajo y los informes se eliminan con él.

Conserve los resultados, no sólo la última ejecución

Un archivo XML de JUnit por commit responde a una pregunta: si pasó. No indica cuándo un endpoint empezó a responder más lento, porque nadie lee esos archivos cuando deja de abrirlos. Para obtener una tendencia, añada una fila por ejecución a una base de datos pequeña en el mismo equipo. Basta con una sola tabla que contenga el SHA del commit, el nombre del archivo, el número de pruebas correctas, el número de pruebas fallidas y la duración. SQLite en producción en un VPS es una opción razonable: un solo archivo, sin proceso de servidor, y todo el historial se incluye en la copia de seguridad que ya realiza. Analice la salida --report-json de Hurl en lugar del XML de JUnit, porque es el formato legible por máquinas de los dos.

Qué debe conservarse tras reconstruir un contenedor

Las definiciones de mocks y las suites de pruebas son código fuente. Deben estar en un repositorio junto al servicio que describen y modificarse en la misma pull request que cambia un endpoint. Un stub editado en una interfaz web o una expectativa enviada a MockServer mediante su API REST durante la ejecución sólo existe en la memoria de ese contenedor o en la base de datos de esa herramienta. Ejecute docker compose down y desaparecerá. Nadie lo detectará hasta que una prueba empiece a pasar por el motivo equivocado. Si sus repositorios también se ejecutan en su propio hardware, un servidor Git autogestionado mantiene los fixtures y el servicio dentro del mismo límite de confianza.

Estas son las reglas prácticas. Fije las etiquetas de las imágenes, porque latest puede cambiar la forma en que el mock compara las peticiones sin que cambie nada en su repositorio. Es muy difícil relacionar ese fallo con su causa. Monte los directorios de stubs como sólo lectura cuando la herramienta no necesite escribir en ellos. Nunca coloque los stubs de un mock en un volumen Docker con nombre, porque el volumen se convierte entonces en la fuente de verdad y la copia del repositorio Git queda desactualizada sin indicarlo.

Hay otra consideración importante que suele pasar desapercibida. Si crea stubs registrando tráfico real a través de un proxy, lea todos los archivos generados antes de confirmarlos. Una grabación contiene exactamente lo que devolvió el upstream, incluidos los bearer tokens y las direcciones de correo de los clientes. Al confirmarla, ese contenido queda permanentemente en el repositorio, porque Git conserva el contenido eliminado en su historial.

FAQ

¿Cuál es la diferencia entre un servidor simulado de API y un ejecutor de pruebas de API?

Un servidor simulado responde a las solicitudes. Sustituye una dependencia a la que no puede llamar desde CI y nunca informa de si una prueba se aprueba o falla. Un ejecutor de pruebas de API envía solicitudes a su propio servicio, comprueba las respuestas, transfiere valores de una llamada a la siguiente y termina con un código distinto de cero cuando falla una comprobación. Resuelven problemas diferentes. Una configuración habitual ejecuta ambos al mismo tiempo: el ejecutor llama a su servicio y su servicio llama al servidor simulado.

¿Puedo probar una API interna desde un ejecutor de CI alojado?

No, a menos que la exponga. Un ejecutor alojado se encuentra fuera de su red, por lo que no puede acceder a un servicio enlazado a una dirección privada. Sus opciones son publicar la API, ejecutar un túnel o mantener una copia pública de staging. Cada opción añade un sistema que puede fallar o filtrar información. Un ejecutor en la misma red privada llama directamente al servicio. Esta es la principal razón práctica por la que los equipos alojan este trabajo en su propia infraestructura.

¿Dónde deben almacenarse los stubs simulados y las suites de pruebas de API?

En git, junto al servicio que describen. Las herramientas que almacenan las definiciones como archivos, como el directorio mappings/ de WireMock, el archivo de datos de Mockoon, los archivos de Hurl y la carpeta .bru de Bruno, permiten revisar el código y reconstruir un contenedor sin coste adicional. Las herramientas que almacenan las definiciones en una base de datos o en una interfaz web necesitan un plan de copias de seguridad y un paso de exportación. La exportación es la parte que la gente olvida hasta que el contenedor ya ha desaparecido.

¿Por qué mi servidor simulado devuelve 404 cuando el stub parece correcto?

WireMock sirve un stub sólo cuando hay una coincidencia exacta. Una solicitud sin coincidencia recibe 404 con un cuerpo que comienza por Request was not matched, seguido de una diferencia con respecto al stub más cercano. Esa diferencia indica el campo que no coincide. Las causas habituales son una barra final en la ruta, una cabecera Content-Type que requiere el stub pero que el cliente no envió, el uso de urlPath cuando el stub necesita urlPathPattern para un segmento variable y un comparador del cuerpo que no se ajusta a la carga útil. Compruebe primero /__admin/requests para confirmar que la solicitud llegó al servidor simulado.

¿Sigo necesitando servidores simulados si tengo un entorno de staging?

Sí, por dos motivos. Una copia de staging de un servicio ascendente que no controla también puede dejar de estar disponible y aplicar límites de tasa. Por tanto, la suite falla por motivos que no tienen relación con su código. Además, no puede generar las respuestas que más necesita probar, como una tarjeta rechazada o un tiempo de espera agotado en la pasarela. Un servidor simulado devuelve esas respuestas cuando se solicitan y a la velocidad de la red local. Así, una suite que tarda minutos contra un sandbox puede tardar segundos. Mantenga staging para la comprobación final antes de una versión y use servidores simulados en CI.