Ir al contenido
wannacode

API e interfaz de usuario

En resumen: Una prueba de componente llama a la regla, pero no ve cómo la usa el servidor. Una prueba de API envía una petición como un cliente real y comprueba el código de estado y el cuerpo: así atrapa un servidor que copió la regla y olvidó el rechazo. Una prueba de interfaz abre un navegador y encuentra los botones por rol y nombre, como los ve una persona. Es lenta y frágil, así que hay pocas y no se ejecutan con cada cambio.

Problema

Todo está en verde, y el carrito vacío pasa

Los escenarios pasan. Las pruebas de componente pasan. El viernes la tienda lanza una versión nueva, y el lunes en la base de datos hay pedidos de 0,00 € con 4,90 € de envío pagado.

La regla de la clase del envío es correcta. Pero el programador del servidor «optimizó» y calculó la tarifa directamente en el código que responde a las peticiones. No copió el rechazo. Ninguna prueba pasó por el servidor.

La idea

La prueba de API

El servidor de la tienda responde a peticiones. La aplicación del cliente pregunta GET /delivery?total=4999 y recibe la tarifa. Para un carrito vacío recibe el código de estado 400 y una explicación:

src/main/java/shop/ShopApi.java
private static void delivery(HttpExchange exchange) throws IOException {
int total = Integer.parseInt(exchange.getRequestURI().getQuery().replace("total=", ""));
try {
send(exchange, 200, String.valueOf(Delivery.feeFor(total)));
} catch (IllegalArgumentException e) {
send(exchange, 400, e.getMessage());
}
}

Una prueba de API es un cliente de este servidor. Arranca el servidor, envía una petición HTTP real y comprueba dos cosas: el código de estado y el cuerpo. No ve el interior del servidor, solo su contrato con el cliente:

src/test/java/shop/DeliveryApiTest.java
@Test
@DisplayName("carrito vacío → 400")
void emptyCartIsRefused() throws Exception {
HttpResponse<String> response = get(0);
assertEquals(400, response.statusCode());
assertEquals("Un carrito vacío no se puede pedir", response.body());
}

El servidor arranca en el mismo proceso, en un puerto libre que elige el sistema. Así la prueba no depende de nada fuera de ella y se puede ejecutar en cualquier sitio.

Más precisamente, para curiosos

Los servidores de aquí son los más pequeños que trae cada lenguaje: com.sun.net.httpserver en Java, ASP.NET Core Minimal API en C#, http.server en Python y node:http en JavaScript. En un proyecto real el servidor es grande, pero la prueba es la misma: una petición, un código de estado, un cuerpo. Java y C# tienen además herramientas que arrancan la aplicación en modo de prueba sin una red real (por ejemplo WebApplicationFactory en ASP.NET Core). Python y JavaScript tienen las suyas (TestClient en FastAPI, supertest).

La idea

La ejecución

El archivo entero de la prueba de API, contra el servidor que llama a la regla. Todo pasa:

src/test/java/shop/DeliveryApiTest.java
package shop;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import static org.junit.jupiter.api.Assertions.assertEquals;
class DeliveryApiTest {
private static HttpServer server;
private static final HttpClient client = HttpClient.newHttpClient();
@BeforeAll
static void start() throws Exception {
server = ShopApi.start(0);
}
@AfterAll
static void stop() {
server.stop(0);
}
private HttpResponse<String> get(int total) throws Exception {
URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/delivery?total=" + total);
return client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString());
}
@Test
@DisplayName("49,99 € → envío 4,90 €")
void paidDelivery() throws Exception {
HttpResponse<String> response = get(4999);
assertEquals(200, response.statusCode());
assertEquals("490", response.body());
}
@Test
@DisplayName("50,00 € → envío gratis")
void freeDelivery() throws Exception {
HttpResponse<String> response = get(5000);
assertEquals(200, response.statusCode());
assertEquals("0", response.body());
}
@Test
@DisplayName("carrito vacío → 400")
void emptyCartIsRefused() throws Exception {
HttpResponse<String> response = get(0);
assertEquals(400, response.statusCode());
assertEquals("Un carrito vacío no se puede pedir", response.body());
}
}
Salida
OK 49,99 € → envío 4,90 €
OK 50,00 € → envío gratis
OK carrito vacío → 400
Pruebas: 3, pasan: 3, fallan: 0

Ahora el servidor del principio de la lección: la regla está copiada, sin el rechazo.

src/main/java/shop/ShopApi.java
private static void delivery(HttpExchange exchange) throws IOException {
int total = Integer.parseInt(exchange.getRequestURI().getQuery().replace("total=", ""));
// La regla está copiada aquí en lugar de llamar a Delivery.
int fee = total >= Delivery.FREE_FROM ? 0 : Delivery.FEE;
send(exchange, 200, String.valueOf(fee));
}

Las mismas pruebas fallan con el carrito vacío. El servidor dice 200, y debería decir 400:

src/test/java/shop/DeliveryApiTest.java
package shop;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import static org.junit.jupiter.api.Assertions.assertEquals;
class DeliveryApiTest {
private static HttpServer server;
private static final HttpClient client = HttpClient.newHttpClient();
@BeforeAll
static void start() throws Exception {
server = ShopApi.start(0);
}
@AfterAll
static void stop() {
server.stop(0);
}
private HttpResponse<String> get(int total) throws Exception {
URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/delivery?total=" + total);
return client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString());
}
@Test
@DisplayName("49,99 € → envío 4,90 €")
void paidDelivery() throws Exception {
HttpResponse<String> response = get(4999);
assertEquals(200, response.statusCode());
assertEquals("490", response.body());
}
@Test
@DisplayName("50,00 € → envío gratis")
void freeDelivery() throws Exception {
HttpResponse<String> response = get(5000);
assertEquals(200, response.statusCode());
assertEquals("0", response.body());
}
@Test
@DisplayName("carrito vacío → 400")
void emptyCartIsRefused() throws Exception {
HttpResponse<String> response = get(0);
assertEquals(400, response.statusCode());
assertEquals("Un carrito vacío no se puede pedir", response.body());
}
}
Salida
FALLA carrito vacío → 400: expected: <400> but was: <200>
OK 49,99 € → envío 4,90 €
OK 50,00 € → envío gratis
Pruebas: 3, pasan: 2, fallan: 1

Las pruebas de componente de la lección anterior siguen pasando, porque la clase del envío no se ha tocado. Cada nivel atrapa sus propios errores.

Pruébalo

¿Qué ve solo la prueba de API?

1. Las pruebas de componente pasan. ¿Qué más puede estar roto y verlo solo la prueba de API?
2. Un carrito vacío tiene que rechazarse. La prueba de API comprueba solo el cuerpo «Un carrito vacío no se puede pedir». ¿Qué se pierde?
3. ¿Por qué la prueba de API arranca el servidor en un puerto libre (el puerto 0) y no en el 8080?

La idea

La prueba de interfaz con Playwright

Una prueba de interfaz (UI test) abre un navegador real y hace lo que hace el cliente: abre el carrito, pulsa «Hacer el pedido», lee lo que dice. La herramienta aquí es Playwright. Tiene versiones para los cuatro lenguajes:

src/test/java/shop/CheckoutUiTest.java
package shop;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.options.AriaRole;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
@Tag("ui")
class CheckoutUiTest {
@Test
@DisplayName("pedido justo en el límite")
void freeDeliveryOnTheBoundary() {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
Page page = browser.newPage();
// un carrito de 50,00 €
page.navigate("http://localhost:4100/cart?test-cart=5000");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Hacer el pedido")).click();
assertThat(page.getByRole(AriaRole.HEADING, new Page.GetByRoleOptions().setName("Pedido aceptado"))).isVisible();
assertThat(page.getByText("Envío: gratis")).isVisible();
}
}
}

Fíjate en cómo la prueba encuentra el botón: por rol y nombre («un botón llamado Hacer el pedido»), no por CSS (#app > div:nth-child(3) > button). El rol y el nombre son lo que ve el cliente y lo que un lector de pantalla le dice a una persona ciega. Cambian cuando cambia el significado. El CSS cambia cuando el diseñador mueve algo.

Las comprobaciones de Playwright esperan solas: «el encabezado es visible» espera hasta unos segundos a que aparezca. Por eso no hay pausas en la prueba.

Más precisamente, para curiosos

El carrito de prueba (?test-cart=5000) es un acuerdo con los programadores: en el entorno de pruebas la tienda puede arrancar con un carrito ya preparado, para que la prueba de interfaz no tenga que buscar y añadir productos. Es una parte de la prueba que vive no en Playwright sino en la tienda.

Playwright es de Microsoft y es de código abierto. Otras herramientas extendidas son Selenium (la más antigua) y Cypress (solo JavaScript). Localizar por rol lo popularizó Testing Library, y los roles son los del estándar de accesibilidad ARIA.

La idea

Por qué las pruebas de interfaz se rompen y por qué hay pocas

Una prueba de interfaz es la más cercana al cliente y la más cara:

  • lenta: un navegador, una red, una página. Segundos por prueba, no milisegundos;
  • necesita todo: una tienda en marcha, una base de datos, datos de prueba;
  • frágil: falla por un botón que se movió, una red lenta, una animación. Una prueba frágil (brittle) falla sin un defecto, y la gente deja de confiar en ella.

Por eso hay pocas pruebas de interfaz: solo los caminos de punta a punta que no pueden romperse. Las reglas se comprueban más abajo: los importes en el nivel de componente, el contrato del servidor con una prueba de API.

Esta prueba de interfaz no se ejecuta en el curso: necesita un navegador y una tienda en marcha, que el verificador del curso no tiene. Por la misma razón, en la configuración de CI de la siguiente lección las pruebas de interfaz se mantienen aparte y se ejecutan una vez por noche contra el entorno de pruebas, no con cada cambio. Los escenarios, las pruebas de componente y las pruebas de API de este módulo sí se ejecutan de verdad.

Pruébalo

¿Qué localizador sobrevive?

1. ¿Qué localizador sobrevive si el diseñador mueve el botón «Hacer el pedido» a otro bloque y cambia sus clases?
2. El botón es <div class="btn" onclick="…">Hacer el pedido</div>. Una prueba que busca el rol «button» no lo encuentra. ¿Qué significa?
3. Después de «Hacer el pedido» la confirmación aparece al cabo de medio segundo. ¿Cómo la espera la prueba?

Ejercicio

Qué nivel para qué

1. API, interfaz o más abajo. La tienda está ordenando sus pruebas.

1. ¿Qué se comprueba mejor con una prueba de API?
2. La tienda tiene 400 pruebas de interfaz, se ejecutan durante 2 horas, y cada día fallan cinco o seis sin que haya un defecto. ¿Qué propones?
3. ¿Qué prueba de interfaz vale más?

Test

Ponte a prueba

1. ¿Qué comprueba una prueba de API?
2. ¿Por qué una prueba de interfaz encuentra los elementos por rol y nombre y no por CSS?
3. ¿Por qué hay pocas pruebas de interfaz?
4. El servidor copió la regla y olvidó el rechazo. ¿Qué prueba lo atrapa?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·