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.
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:
- Solo ver los endpoints y el flujo CRUD → saltá a "El flujo completo"
- Entender cómo descubrí los endpoints → leé "Investigación con DevTools"
- Ver los escenarios negativos → saltá a "¿Qué pasa si...?"
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:
- Investigar los endpoints con DevTools (misma técnica que al principio de la serie)
- Armar los requests en Postman
- Correr el ciclo completo con el Runner
- 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.

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:

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

{
"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:

{"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

le di Eliminar, confirmé:

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

{"EntityId": "CESBS"}
La Response:

{"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)

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.

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.

All 21 Passed 21 Failed 0 Skipped 0
El ciclo completo funciona:
- Login + Refresh Token → autenticación
- Customer List → baseline: 91 registros
- Create Customer → 200, EntityId: CESBS
- Customer List (Verify Create) → 92 registros, CESBS existe
- Delete Customer → 200, WasAlreadyDeleted: false
- 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.

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.

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.