Retrieve y Update en Postman: completando el CRUD

Obtengo los datos de un cliente con Retrieve, modifico un campo con Update y verifico el cambio. Ciclo CRUD completo sobre la misma entidad.

Postman Runner 30 passed 0 failed showing Retrieve, Update, Verify Update, Delete all green, right panel with Verify Update response showing ContactName UPDATED
Runner 30/30: Retrieve, Update, Verify Update, Delete, todo verde. A la derecha, la response del Verify Update confirmando el cambio en ContactName.

El post anterior cubrió Create y Delete: crear un cliente, verificar, eliminarlo, confirmar. Faltaba la U del CRUD. Este post la completa.

El flujo es: Retrieve para obtener los datos actuales → Update para modificarlos → Retrieve de nuevo para verificar que el cambio se aplicó. Todo sobre CESBS, el mismo cliente que creo y elimino en cada corrida.


Investigación con DevTools

Misma técnica que en los posts anteriores: abrir DevTools, Network, Fetch/XHR, y operar desde la UI de Serenity observando qué requests salen.

Retrieve: obtener un cliente

Abrí el cliente ALFKI desde la grilla de Clientes.

Serenity Demo edit customer form for Alfreds Futterkiste with DevTools Network showing Retrieve request to Services Northwind Customer Retrieve, POST method, 200 OK
Al abrir un cliente, Serenity llama a /Customer/Retrieve. POST, 200 OK.

Al abrir un cliente, Serenity llama a /Customer/Retrieve. POST, 200 OK.

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

La Response:

DevTools Response tab showing Entity object with CustomerID ALFKI, CompanyName Alfreds Futterkiste, ContactName Maria Anders, Address Obere Str 57, City Berlin, Country Germany and all customer fields
Retrieve devuelve el objeto Entity completo con todos los campos del cliente.

Retrieve devuelve el objeto Entity completo con todos los campos del cliente.

{
  "Entity": {
    "CustomerID": "ALFKI",
    "CompanyName": "Alfreds Futterkiste",
    "ContactName": "Maria Anders",
    "ContactTitle": "Sales Representative",
    "Address": "Obere Str. 57",
    "City": "Berlin",
    "Country": "Germany",
    "PostalCode": "12209",
    "Phone": "030-0074321",
    "Fax": "030-0076545",
    "NoteList": [],
    "Representatives": []
  }
}

Mismo wrapper "Entity" que en Create y Customer List. La convención de Serenity es consistente.

En el post anterior mapeé el Retrieve pero no lo usé. Acá entra como request real: necesito los datos del cliente para saber qué tengo antes de modificar.

Update: modificar un cliente

Sin cerrar el formulario de ALFKI, cambié el ContactName a "cesar beas suarez" y le di Guardar.

Serenity Demo customer grid with ALFKI ContactName changed, DevTools showing Update request POST Services Northwind Customer Update 200 OK
Update: POST /Services/Northwind/Customer/Update, 200 OK. Al guardar, Serenity dispara Update, un Lookup y un List para refrescar la grilla.

Update: POST /Services/Northwind/Customer/Update, 200 OK. Al guardar, Serenity dispara Update, un Lookup y un List.

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

El Payload:

DevTools Payload tab showing Update request body with all customer fields, ContactName cesar beas suarez, and EntityId ALFKI at root level
El payload del Update: manda TODOS los campos, no solo el modificado. EntityId aparece dentro de Entity y también a nivel raíz.

El payload del Update: manda TODOS los campos, no solo el modificado. Y tiene EntityId tanto dentro de Entity como afuera.

{
  "Entity": {
    "CompanyName": "Alfreds Futterkiste",
    "ContactName": "cesar beas suarez",
    "ContactTitle": "Sales Representative",
    "Representatives": [],
    "Address": "Obere Str. 57",
    "Country": "Germany",
    "City": "Berlin",
    "Region": "",
    "PostalCode": "12209",
    "Phone": "030-0074321",
    "Fax": "030-0076545",
    "NoteList": [],
    "LastContactDate": null,
    "LastContactedBy": "",
    "Email": "",
    "SendBulletin": false,
    "CustomerID": "ALFKI"
  },
  "EntityId": "ALFKI"
}

Dos cosas:

El browser manda todos los campos, no solo el modificado. Yo solo cambié ContactName, pero el payload incluye Address, Phone, Fax, todo. Serenity envía el objeto completo siempre.

EntityId aparece dos veces. Dentro de Entity como CustomerID y afuera como EntityId. En Create el payload solo tenía Entity. En Update tiene ambos. Es la forma que tiene Serenity de identificar qué registro modificar.

La Response:

DevTools Response tab showing EntityId ALFKI as the Update response, customer grid visible with 91 records and ALFKI row showing cesar beas suarez as ContactName
Response del Update: {"EntityId": "ALFKI"}. Mismo formato que Create.

Response del Update: {"EntityId": "ALFKI"}. Mismo formato que Create.

{"EntityId": "ALFKI"}

Mismo formato que Create. Devuelve el ID del registro modificado.

Después del Update, Serenity disparó automáticamente un Lookup y un List (para refrescar la grilla). Esos requests no son parte del Update, los genera la UI.

Mapa de endpoints actualizado

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

El CRUD completo mapeado. Todo POST, la operación la define el endpoint.


Armando los requests en Postman

Body mínimo del Update

El browser mandó todos los campos. Probé con los mínimos, igual que hice con Create:

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

Funcionó. El servidor aceptó el Update con 200. No necesité mandar campos vacíos.

Dónde van los requests nuevos

Los requests de Retrieve y Update tienen que ir entre Create y Delete. Si los ponés después del Delete, CESBS ya no existe y todo falla con EntityNotFound. Esto me pasó en la primera corrida del Runner y tuve que reordenar.

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 - Retrieve Customer               ← NUEVO
POST - Update Customer                  ← NUEVO
POST - Retrieve Customer (Verify Update) ← NUEVO
POST - Delete Customer
POST - Customer List (Verify Delete)
POST - Update Customer (Non-existent)   ← NUEVO (negativo)
Postman collection sidebar showing 16 requests: Login, auth tests, Customer List, Create, Verify, Retrieve, Update, Verify Update, Delete, Verify Delete, Update Non-existent
La colección completa: 16 requests. Retrieve y Update entre Verify Create y Delete.

La colección completa: 16 requests. Retrieve y Update entre Verify Create y Delete.

Pre-request scripts: misma lógica

Todos los requests nuevos usan el mismo pre-request script de siempre:

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

Nada nuevo acá.


Assertions

POST - Retrieve Customer

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

pm.test("Entity is CESBS", function () {
    const jsonData = pm.response.json();
    pm.expect(jsonData.Entity.CustomerID).to.eql("CESBS");
    pm.expect(jsonData.Entity.ContactName).to.eql("Cesar Beas Suarez");
});

Valido que el Retrieve devuelva el cliente correcto con el ContactName original. Este es el estado "antes" del Update.

Postman Retrieve Customer Scripts tab with assertions validating status 200, CustomerID CESBS and ContactName Cesar Beas Suarez
Retrieve Customer: valido que el Entity devuelva CESBS con el ContactName original. Este es el estado "antes" del Update.

Retrieve Customer: 200, Entity con CustomerID CESBS y ContactName original.

POST - Update Customer

Body:

{
  "Entity": {
    "CustomerID": "CESBS",
    "CompanyName": "CesarBeasSuarez Industries",
    "ContactName": "Cesar Beas Suarez UPDATED",
    "ContactTitle": "SEO",
    "Country": "Argentina",
    "City": "Buenos Aires",
    "Phone": "3517898989"
  },
  "EntityId": "CESBS"
}
Postman POST Update Customer request showing Body tab with raw JSON containing Entity with CustomerID CESBS, CompanyName CesarBeasSuarez Industries, ContactName Cesar Beas Suarez UPDATED highlighted with green arrow, ContactTitle SEO, Country Argentina, City Buenos Aires, Phone, and EntityId CESBS
Body del Update: cambio ContactName a "UPDATED". El resto de los campos queda igual. EntityId a nivel raíz identifica qué registro modificar.

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.EntityId).to.eql("CESBS");
});

El Update devuelve {"EntityId": "CESBS"}. Confirma que el registro fue modificado. Pero no dice qué cambió. Para eso necesito un segundo Retrieve.

Assertions del Update: valido 200 y que el EntityId devuelto sea CESBS.
Assertions del Update: valido 200 y que el EntityId devuelto sea CESBS.

Update Customer: 200, EntityId CESBS. El servidor aceptó la modificación.

POST - Retrieve Customer (Verify Update)

Body: mismo que el primer Retrieve ({"EntityId": "CESBS"}).

Post-response:

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

pm.test("ContactName was updated", function () {
    const jsonData = pm.response.json();
    pm.expect(jsonData.Entity.ContactName).to.eql("Cesar Beas Suarez UPDATED");
});

pm.test("Other fields unchanged", function () {
    const jsonData = pm.response.json();
    pm.expect(jsonData.Entity.CompanyName).to.eql("CesarBeasSuarez Industries");
    pm.expect(jsonData.Entity.CustomerID).to.eql("CESBS");
});

Acá valido dos cosas: que el campo modificado cambió y que los otros campos no se alteraron. Si el Update sobreescribiera todo el registro, CompanyName podría haber desaparecido. No fue el caso.

Postman POST Retrieve Customer Verify Update request showing Scripts tab with three post-response assertions: status 200, ContactName equals Cesar Beas Suarez UPDATED, and Other fields unchanged checking CompanyName and CustomerID
Verify Update: valido que ContactName cambió a "UPDATED" y que CompanyName y CustomerID no se alteraron.

Retrieve Verify: ContactName cambió a "UPDATED", CompanyName sigue igual.


¿Qué pasa si...?

Update de un cliente que no existe

Mandé un Update con EntityId "XXXXX":

{
  "Entity": {
    "CustomerID": "XXXXX",
    "CompanyName": "No Existe",
    "ContactName": "Nadie"
  },
  "EntityId": "XXXXX"
}
Postman Runner results showing All 8 Passed 8 Failed 0 with Login Page, Login, Refresh Token and Update Customer Non-existent all passing, right panel showing Update Non-existent response with Error Code EntityNotFound and Message Record not found
Update de registro inexistente: 400 con EntityNotFound. Mismo error que Delete, misma estructura, mismo mensaje.

Update de registro inexistente: 400 con EntityNotFound. Mismo patrón que Delete.

Status: 400 Bad Request

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

Mismo error que el Delete de un registro inexistente. Exactamente el mismo Code, el mismo Message. Serenity usa el mismo patrón de error para cualquier operación sobre un registro que no existe.

Patrón de errores actualizado

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.
Update inexistente 400 EntityNotFound Record not found.

Delete y Update comparten el mismo error para registros inexistentes. Consistente.


El flujo completo: 30/30

Limpié el cookie jar, corrí el Runner.

Postman Runner showing All 30 Passed 30 Failed 0 results with Retrieve Customer, Update Customer, Retrieve Customer Verify Update, Delete Customer, Customer List Verify Delete and Update Customer Non-existent all green, right panel displaying Verify Update response with Entity showing ContactName Cesar Beas Suarez UPDATED
30 assertions, todas verdes. A la derecha, el Retrieve confirma que ContactName se actualizó correctamente.

30 assertions, todas verdes. El ciclo CRUD completo funciona.

All 30  Passed 30  Failed 0  Skipped 0

El ciclo completo sobre CESBS:

  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. Retrieve Customer → 200, Entity con datos originales
  6. Update Customer → 200, EntityId: CESBS
  7. Retrieve Customer (Verify Update) → ContactName UPDATED, otros campos sin cambios
  8. Delete Customer → 200, WasAlreadyDeleted: false
  9. Customer List (Verify Delete) → 91 registros, CESBS no existe
  10. Update Customer (Non-existent) → 400, EntityNotFound

La colección sigue siendo self-cleaning. Create al principio, Delete al final. El Update no rompe eso porque modifica un registro que después se elimina igual.


Takeaways

Retrieve como paso previo al Update. No es solo para verificar después. Es útil antes: te muestra el estado actual del registro, confirmás que existe, y tenés un baseline contra el cual comparar después del Update.

El browser manda todo, el servidor acepta mínimos. El payload del Update desde la UI incluía todos los campos. Desde Postman mandé solo los necesarios y funcionó. Mismo hallazgo que con Create.

EntityId duplicado en el Update. A diferencia de Create (que solo tiene Entity), Update necesita EntityId también a nivel raíz. Es la convención de Serenity para identificar qué registro modificar. Algo a tener en cuenta si estás armando requests sin copiar del browser.

El orden importa. Mi primera corrida falló porque los requests de Retrieve y Update estaban después del Delete. CESBS ya no existía. Reordené y todo pasó. En una colección secuencial, el orden de los requests es parte del diseño del test.


Estado actual

La colección tiene 16 requests y 30 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 completo: Create, Retrieve, Update, Delete con verificaciones
  • Escenarios negativos: create duplicado, delete inexistente, update inexistente
  • La colección es self-cleaning

Lo que sigue: validación contra datos externos

Con el CRUD completo, el siguiente paso es comparar los datos de la grilla contra una fuente externa. Exportar los clientes esperados a un archivo (Excel o CSV) y validar desde Postman que lo que devuelve la API coincide con lo esperado.