Publica un fallo en producción y pierdes una tarde. Publica una suite de pruebas que pasa pero que nunca probó nada de verdad y pierdes mucho más que eso, porque no te enterarás hasta la tarde en que menos te lo esperas. Este capítulo cubre qué probar, qué no molestarse en probar, cómo probar una API que se apoya en tres o cuatro servicios que no controlas, y cómo mantener la suite lo bastante rápida como para que tu equipo la ejecute de verdad. Una prueba que no comprueba el contrato no está probando tu API. Está probando que PHP sigue funcionando.
Que deberia comprobar una prueba de API
La pregunta que toda prueba debería responder antes de escribir una sola aserción: ¿de qué depende quien consume este endpoint? No qué hace el controlador por dentro. No a qué método llamó la facade. Quien consume ve tres cosas: el código de estado HTTP, la forma del JSON y el efecto secundario que le prometiste —un correo enviado, un job encolado, un registro creado. Prueba eso. Nada más sobrevive a una refactorización.
// tests/Feature/LicenseControllerTest.php
test('returns 200 with license data when authorized', function () {
$response = getJson('/api/v1/licenses', [
'Authorization' => "Bearer {$this->token->token}",
]);
$response->assertStatus(Response::HTTP_OK);
$response->assertJsonStructure([
'items' => [
'*' => [
'key',
'name',
'domains',
'created_at',
],
],
]);
});
Fíjate en lo que esta prueba no hace. No entra en LicenseController a comprobar qué método privado corrió. No afirma que LicenseDTOMapper::toDTO() se llamó con argumentos concretos. Golpea la ruta como lo haría un consumidor real, con un token real, y comprueba la respuesta como lo haría un consumidor real: primero el estado, luego la estructura. Si mañana cambias el mapper interno por otra cosa, a esta prueba le da igual, mientras /api/v1/licenses siga devolviendo { "items": [...] } con la forma documentada.
// Bad: asserts on a collaborator, not the contract
test('creates a license', function () {
LicenseFacade::shouldReceive('addLicense')
->once()
->andReturn(['key' => 'new-license-key', 'name' => 'New License', 'domains' => ['newdomain.com'], 'created_at' => '2026-01-29T12:00:00Z']);
$this->postJson('/api/v1/license', ['name' => 'New License', 'domain' => 'newdomain.com']);
});
// Good: asserts on what a consumer actually sees
test('returns created license in response data', function () {
$response = $this->postJson('/api/v1/license', [
'name' => 'New License',
'domain' => 'newdomain.com',
], ['Authorization' => "Bearer {$this->token->token}"]);
expect($response->json())->toHaveKey('data');
expect($response->json('data'))->toHaveKeys(['key', 'name', 'domains', 'created_at']);
});
La versión mala se rompe en cuanto renombras un método o extraes un paso a un job. No sabe nada de si el cliente recibió realmente una respuesta usable: sólo confirma que algún cableado interno se disparó. La versión buena se rompe únicamente cuando cambia el contrato, que es exactamente cuando quieres enterarte.