CRUD real desde Postman: crear, verificar, eliminar y confirmar

Creo un cliente vía API, verifico que aparezca en la grilla con 92 registros, lo elimino y confirmo que vuelve a 91. Ciclo CRUD self-cleaning.

Postman Runner 21 passed 0 failed showing CRUD requests: Create Customer, Verify Create, Delete Customer and Verify Delete all green
El ciclo CRUD completo: crear, verificar, eliminar, confirmar. La colección se limpia sola.

Hasta ahora toda la colección consultaba datos. Este post pasa a modificarlos: creo un cliente, verifico que exista en la grilla, lo elimino y confirmo que desapareció. Ciclo completo, self-cleaning.

Nota para el lector

Este post es parte de la serie de API Testing con Postman sobre Serenity Demo. Los posts anteriores cubren el análisis con DevTools, la construcción de requests, el manejo del CSRF token, assertions con pm.test, testing negativo del login, y el debugging del Runner.

Si querés ir al grano:


Contexto

Hasta este post, toda la colección era de lectura: login, listar clientes. Las requests eran GET y POST que consultaban datos sin modificar nada.

El paso natural era testear operaciones que modifiquen datos. Crear un cliente, verificar que apareció, eliminarlo, confirmar que desapareció. Eso es CRUD real.

El plan:

  1. Investigar los endpoints con DevTools (misma técnica que al principio de la serie)
  2. Armar los requests en Postman
  3. Correr el ciclo completo con el Runner
  4. Probar escenarios negativos: ¿qué pasa si creo un duplicado? ¿Qué pasa si elimino algo que no existe?

Investigación con DevTools

No tenía documentación de la API de Serenity Demo para operaciones de escritura. Así que hice lo mismo que al inicio de la serie: abrí DevTools, Network, filtro Fetch/XHR, y operé desde la UI observando qué requests salían.

Crear un cliente

Abrí la grilla de Clientes, clickeé "Nuevo Cliente", completé los campos y le di Guardar.

Serenity Demo new customer form with fields filled and DevTools Network showing three Lookup requests for Employee, CustomerCountry and CustomerCity
Mientras lleno el formulario, Serenity dispara 3 Lookups para cargar los dropdowns. Todavía no es el Create.

Mientras llenaba el formulario aparecieron 3 requests de Lookup: Employee, CustomerCountry, CustomerCity. Son requests que Serenity dispara automáticamente para cargar los dropdowns. No son el Create.

El request importante apareció al clickear Guardar:

DevTools Network showing Create request with POST method to Services Northwind Customer Create, Status 200 OK, with CESBS visible in the customer grid
El request Create: POST /Services/Northwind/Customer/Create, 200 OK. CESBS aparece en la grilla con 92 registros.

Endpoint: POST /Services/Northwind/Customer/Create Status: 200 OK

El Payload:

DevTools Payload tab showing raw JSON body of Create request with Entity object containing CustomerID CESBS and all customer fields
El payload completo que mandó el browser. La API usa "Entity" como wrapper, mismo patrón que Customer List.
{
  "Entity": {
    "CustomerID": "CESBS",
    "CompanyName": "CesarBeasSuarez Industries",
    "ContactName": "Cesar Beas Suarez",
    "ContactTitle": "SEO",
    "Representatives": ["2"],
    "Address": "Calle 1000",
    "Country": "Argentina",
    "City": "Buenos Aires",
    "Region": "",
    "PostalCode": "5000",
    "Phone": "3517898989",
    "Fax": "",
    "NoteList": [],
    "LastContactDate": null,
    "LastContactedBy": "2",
    "Email": "[email protected]",
    "SendBulletin": false
  }
}

La API usa "Entity" como wrapper del objeto. Mismo patrón que Customer List. Serenity tiene una convención clara.

La Response:

DevTools Response showing EntityId CESBS, customer grid with CESBS row highlighted and green arrows pointing to the new record and total count of 92
Response: {"EntityId": "CESBS"}. La grilla confirma: 92 registros, CESBS visible.
{"EntityId": "CESBS"}

Simple. Devuelve el ID del cliente creado. La grilla se refrescó sola y mostró 92 registros (antes eran 91). CESBS aparecía en la lista.

Eliminar un cliente

Abrí el cliente CESBS desde la grilla

Serenity Demo edit customer form for CesarBeasSuarez Industries with DevTools showing Retrieve request to Services Northwind Customer Retrieve, 200 OK
Al abrir el cliente, Serenity llama a /Customer/Retrieve para traer los datos. No lo uso en el test, pero queda mapeado.

le di Eliminar, confirmé:

DevTools Network showing Delete request with POST method to Services Northwind Customer Delete, Status 200 OK, grid showing 91 records
Delete: POST /Services/Northwind/Customer/Delete, 200 OK. La grilla volvió a 91 registros.

Endpoint: POST /Services/Northwind/Customer/Delete Status: 200 OK

El Payload:

DevTools Payload tab showing Delete request body with EntityId CESBS
Payload del Delete: solo el EntityId. Simple.
{"EntityId": "CESBS"}

La Response:

DevTools Response tab showing WasAlreadyDeleted false for Delete request
WasAlreadyDeleted: false. El registro existía y fue borrado en este momento.
{"WasAlreadyDeleted": false}

WasAlreadyDeleted: false significa que el registro existía y fue borrado en este momento. La grilla volvió a 91.

Detalle: todo es POST

Serenity no usa los métodos HTTP convencionales (PUT para update, DELETE para delete). Todo es POST. El endpoint define la operación, no el método HTTP. Es una convención de Serenity/Serenity.is, no del estándar REST. Algo a tener en cuenta.

Mapa de endpoints descubierto

Operación Endpoint Método
Crear /Services/Northwind/Customer/Create POST
Listar /Services/Northwind/Customer/List POST
Eliminar /Services/Northwind/Customer/Delete POST
Obtener /Services/Northwind/Customer/Retrieve POST

El Retrieve lo vi cuando abrí el cliente para editarlo: Serenity llama a /Customer/Retrieve para traer todos los datos. No lo uso en este post, pero queda mapeado.


Armando los requests en Postman

Simplificar el body del Create

El body que mandó el browser tenía todos los campos, incluyendo vacíos ("Fax": "", "NoteList": [], "Region": ""). Para el test no necesito todo eso. Probé con los campos mínimos:

{
  "Entity": {
    "CustomerID": "CESBS",
    "CompanyName": "CesarBeasSuarez Industries",
    "ContactName": "Cesar Beas Suarez",
    "ContactTitle": "SEO",
    "Country": "Argentina",
    "City": "Buenos Aires",
    "Phone": "3517898989"
  }
}

Funcionó. El servidor aceptó el Create con 200 y devolvió {"EntityId": "CESBS"}. Los campos vacíos no son obligatorios.

Un body limpio con pocos campos se lee mejor y es más fácil de mantener.

Estructura de la colección

La colección quedó así:

GET  - Login Page
POST - Login
GET  - Refresh Token (Post-Login)
POST - Login (user correcto, pass mal)
POST - Login (user mal, pass correcta)
POST - Login (user y pass incorrectos)
POST - Login (user y pass vacíos)
POST - Customer List
POST - Create Customer
POST - Customer List (Verify Create)
POST - Delete Customer
POST - Customer List (Verify Delete)
Postman collection sidebar showing complete Serenity Demo Auth and Customer API Flow with 12 requests including Create Customer, Customer List Verify Create, Delete Customer and Customer List Verify Delete
La colección completa: autenticación, testing negativo del login, consulta de grilla y ciclo CRUD.

Dos cosas sobre la organización:

Nombres descriptivos. Hay tres requests que usan el mismo endpoint (/Customer/List). Sin nombres claros, no sabés cuál es cuál. Customer List es el baseline, Customer List (Verify Create) verifica que el cliente apareció, Customer List (Verify Delete) verifica que desapareció.

Docs en cada request. Postman tiene una pestaña "Docs" en cada request donde podés escribir para qué sirve. Lo usé para documentar el propósito de cada uno, especialmente los que tienen nombres parecidos.

Postman Docs tab for POST Create Customer request showing description: Crea cliente CESBS via API, Valida 200 plus EntityId retornado
Cada request tiene su descripción en la pestaña Docs. Útil cuando hay varios requests con nombres parecidos.

Pre-request scripts: misma lógica que antes

Todos los requests nuevos (Create, Delete, los Customer List de verificación) usan el mismo pre-request script que ya tenía:

const csrf = pm.environment.get("csrfToken");
if (csrf) {
    pm.request.headers.upsert({
        key: "x-csrf-token",
        value: csrf
    });
}

El CSRF token del environment, seteado por el Refresh Token post-login, se manda en cada request. Nada nuevo acá.


Assertions: qué valido en cada paso

POST - Create Customer (post-response)

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

pm.test("EntityId is CESBS", function () {
    const jsonData = pm.response.json();
    pm.expect(jsonData).to.have.property("EntityId");
    pm.expect(jsonData.EntityId).to.eql("CESBS");
});

Valido que el servidor devuelva 200 y que el EntityId sea exactamente "CESBS". Si el servidor rechaza o devuelve otro ID, el test falla.

POST - Customer List (Verify Create) (post-response)

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

pm.test("TotalCount is 92 after create", function () {
    const jsonData = pm.response.json();
    pm.expect(jsonData.TotalCount).to.eql(92);
});

pm.test("CESBS exists in Entities", function () {
    const jsonData = pm.response.json();
    const cesbs = jsonData.Entities.find(e => e.CustomerID === "CESBS");
    pm.expect(cesbs).to.not.be.undefined;
    pm.expect(cesbs.CompanyName).to.eql("CesarBeasSuarez Industries");
});

No alcanza con verificar que TotalCount subió a 92. Alguien podría haber creado otro cliente al mismo tiempo. El test busca específicamente a CESBS dentro del array de Entities y verifica el CompanyName. Eso es más robusto.

POST - Delete Customer

Body:

{
    "EntityId": "CESBS"
}

Post-response:

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

pm.test("WasAlreadyDeleted is false", function () {
    const jsonData = pm.response.json();
    pm.expect(jsonData.WasAlreadyDeleted).to.eql(false);
});

WasAlreadyDeleted: false confirma que el registro existía y fue eliminado ahora. No que ya estaba borrado de antes.

POST - Customer List (Verify Delete) (post-response)

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

pm.test("TotalCount is 91 after delete", function () {
    const jsonData = pm.response.json();
    pm.expect(jsonData.TotalCount).to.eql(91);
});

pm.test("CESBS no longer exists in Entities", function () {
    const jsonData = pm.response.json();
    const cesbs = jsonData.Entities.find(e => e.CustomerID === "CESBS");
    pm.expect(cesbs).to.be.undefined;
});

Misma lógica invertida: TotalCount volvió a 91 y CESBS no aparece en el array.


El flujo completo: 21/21

Limpié el cookie jar, corrí el Runner.

Postman Runner detailed results showing all CRUD assertions passing: EntityId is CESBS, TotalCount is 92 after create, CESBS exists in Entities, WasAlreadyDeleted is false, TotalCount is 91 after delete, CESBS no longer exists
Detalle de las assertions CRUD: EntityId correcto, TotalCount sube a 92, CESBS existe, WasAlreadyDeleted false, TotalCount vuelve a 91, CESBS desapareció.
All 21  Passed 21  Failed 0  Skipped 0

El ciclo completo funciona:

  1. Login + Refresh Token → autenticación
  2. Customer List → baseline: 91 registros
  3. Create Customer → 200, EntityId: CESBS
  4. Customer List (Verify Create) → 92 registros, CESBS existe
  5. Delete Customer → 200, WasAlreadyDeleted: false
  6. Customer List (Verify Delete) → 91 registros, CESBS no existe

La colección es self-cleaning: crea datos, los verifica, los elimina y confirma la eliminación. No deja basura en la base de datos. Cada corrida empieza y termina con el mismo estado.

Validación cross-channel: UI → API

Creé el cliente desde la UI de Serenity y lo eliminé desde Postman. Funcionó sin diferencia. Esto confirma algo que a veces se asume pero no se prueba: los endpoints responden igual independientemente del origen de la operación. La API es la API, no importa si la llama el browser o Postman. En un contexto real, este tipo de validación cruzada es útil para detectar inconsistencias entre lo que la UI envía y lo que la API acepta directamente.


¿Qué pasa si...?

Después de tener el flujo feliz funcionando, probé dos escenarios negativos.

Crear un cliente que ya existe

Corrí el Create con CESBS ya existente en la base.

Postman Create Customer request returning 400 Bad Request with error message Can't save record There is another record with the same Customer Id value
Create duplicado: 400. El servidor rechaza con mensaje claro, no sobreescribe el registro existente.

Status: 400 Bad Request

{
  "Error": {
    "Message": "Can't save record. There is another record with the same Customer Id value!"
  }
}

El servidor rechaza duplicados con un mensaje claro. No crea un segundo registro, no sobreescribe el existente. 400 con el motivo exacto.

Eliminar un cliente que no existe

Corrí el Delete con CESBS ya eliminado.

Postman Delete Customer request returning 400 Bad Request with Error Code EntityNotFound and message Record not found It might be deleted or you don't have required permissions
Delete de registro inexistente: 400 con EntityNotFound. No distingue entre "no existe" y "sin permisos".

Status: 400 Bad Request

{
  "Error": {
    "Code": "EntityNotFound",
    "Message": "Record not found. It might be deleted or you don't have required permissions!"
  }
}

Acá esperaba que el servidor devolviera WasAlreadyDeleted: true. No fue así. Si el registro no existe, directamente tira 400 con EntityNotFound. WasAlreadyDeleted solo aparece cuando el Delete funciona (200).

El mensaje no distingue entre "no existe" y "no tenés permisos". Es la misma práctica de seguridad que vimos en el login: no revelar qué falló exactamente.

Patrón de errores de Serenity

Con los escenarios negativos de este post y del post de testing negativo del login, se confirma un patrón consistente:

Operación Status Error Code Mensaje
Login incorrecto 400 AuthenticationError Invalid username or password!
Create duplicado 400 (sin Code) Can't save record. Same Customer Id value!
Delete inexistente 400 EntityNotFound Record not found.

Siempre 400 + objeto Error con Message. A veces incluye Code, a veces no. Pero la estructura es la misma.


Takeaways

Sobre la investigación de APIs: No necesitaba documentación. DevTools + operar desde la UI fue suficiente para descubrir los endpoints, los payloads y las responses. Es la misma técnica del primer post de la serie, aplicada a operaciones de escritura.

Sobre el body del Create: El browser mandaba todos los campos, incluyendo vacíos. Probé con los mínimos y funcionó. En tests, un body limpio es mejor que uno inflado con campos vacíos.

Sobre colecciones self-cleaning: El ciclo crea → verifica → elimina → confirma es el patrón ideal. La colección no deja datos de prueba en la base. Cada corrida del Runner empieza y termina en el mismo estado. Eso es importante para que los tests sean repetibles.

Sobre Serenity y REST: Serenity no sigue convenciones REST estrictas. Todo es POST, la operación la define el endpoint, no el método HTTP. Esto no es malo, es una convención del framework. Como tester, hay que observar cómo funciona la API real, no asumir que sigue un estándar.


Estado actual

La colección completa tiene 12 requests y 21 assertions que pasan en una corrida limpia. Cubre:

  • Autenticación (login + CSRF token refresh)
  • Testing negativo del login (4 escenarios)
  • Consulta de grilla (Customer List con validación de estructura)
  • CRUD: crear, verificar, eliminar, confirmar
  • La colección es self-cleaning

Lo que sigue: completar el CRUD con Update

Este post cubrió Create y Delete. Falta la U: modificar un cliente existente y verificar que los cambios se aplicaron. El próximo post usa Retrieve para obtener los datos actuales y Update para modificarlos, completando el ciclo CRUD.