gRPC (como se discute en API Testing Architecture: From Monoliths to Microservices) es un framework RPC de alto rendimiento y código abierto desarrollado por Google que utiliza Protocol Buffers para serialización y HTTP/2 para transporte. Probar servicios gRPC requiere enfoques especializados debido a su protocolo binario, capacidades de streaming y tipado fuerte. Esta guía completa cubre todos los aspectos del testing de APIs gRPC.
Entendiendo la arquitectura gRPC
Antes de profundizar en estrategias de testing, es esencial entender los conceptos básicos de gRPC y cómo difieren de las APIs REST tradicionales.
Comparación gRPC vs REST
Característica | REST | gRPC |
---|---|---|
Protocolo | HTTP/1.1 | HTTP/2 |
Formato de Datos | JSON (típicamente) | Protocol Buffers (binario) |
Streaming | Limitado (SSE, WebSockets) | Bidireccional integrado |
Generación de Código | Opcional (OpenAPI) | Requerido (protoc) |
Soporte de Navegador | Nativo | Requiere gRPC-Web |
Rendimiento | Bueno | Excelente (binario, multiplexing) |
Type Safety | Validación en runtime | Validación en tiempo de compilación |
El protocolo binario y la generación de código de gRPC proporcionan seguridad de tipos fuerte pero requieren herramientas de testing diferentes comparadas con las APIs REST (como se discute en GraphQL Testing: Complete Guide with Examples).
Protocol Buffers y Testing de Schemas
Los Protocol Buffers (protobuf) definen el contrato del servicio. El testing comienza validando tus archivos .proto.
Validación de Schema Proto
// user.proto
syntax = "proto3";
package user.v1;
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc ListUsers(ListUsersRequest) returns (stream UserResponse);
rpc StreamUsers(stream GetUserRequest) returns (stream UserResponse);
}
message GetUserRequest {
string user_id = 1;
}
message GetUserResponse {
string user_id = 1;
string name = 2;
string email = 3;
int64 created_at = 4;
}
Linting y Validación de Schema
Usa buf
o herramientas similares para hacer lint de archivos proto:
# Instalar buf
brew install bufbuild/buf/buf
# Crear buf.yaml
version: v1
lint:
use:
- DEFAULT
breaking:
use:
- FILE
// Prueba de validación de schema
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
describe('Pruebas de Schema Proto', () => {
test('debe pasar linting de proto', async () => {
const { stdout, stderr } = await execAsync('buf lint');
expect(stderr).toBe('');
});
test('no debe introducir cambios que rompan compatibilidad', async () => {
try {
await execAsync('buf breaking --against .git#branch=main');
} catch (error) {
fail('Cambios que rompen compatibilidad detectados: ' + error.stdout);
}
});
});
Testing de RPC Unario
Los RPCs unarios son la forma más simple—una sola petición, una sola respuesta. Son análogos a las llamadas API REST tradicionales.
Testing Básico de Llamada Unaria
// Usando @grpc/grpc-js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
describe('Pruebas Unarias de UserService', () => {
let client;
beforeAll(() => {
const packageDefinition = protoLoader.loadSync('user.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const userProto = grpc.loadPackageDefinition(packageDefinition).user.v1;
client = new userProto.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
});
afterAll(() => {
client.close();
});
test('debe obtener usuario por ID', (done) => {
client.GetUser({ user_id: '123' }, (error, response) => {
expect(error).toBeNull();
expect(response.user_id).toBe('123');
expect(response.name).toBeDefined();
expect(response.email).toMatch(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/);
done();
});
});
test('debe manejar error de usuario no encontrado', (done) => {
client.GetUser({ user_id: 'noexiste' }, (error, response) => {
expect(error).toBeDefined();
expect(error.code).toBe(grpc.status.NOT_FOUND);
expect(error.details).toContain('Usuario no encontrado');
done();
});
});
test('debe validar campos requeridos', (done) => {
client.GetUser({}, (error, response) => {
expect(error).toBeDefined();
expect(error.code).toBe(grpc.status.INVALID_ARGUMENT);
done();
});
});
});
Testing de gRPC con Promesas
Envuelve callbacks de gRPC en promesas para sintaxis async/await más limpia:
const { promisify } = require('util');
describe('UserService con Promesas', () => {
let client;
let getUser;
beforeAll(() => {
// ... inicializar client
getUser = promisify(client.GetUser.bind(client));
});
test('debe crear usuario', async () => {
const createUser = promisify(client.CreateUser.bind(client));
const response = await createUser({
name: 'Juan Pérez',
email: 'juan@ejemplo.com',
password: 'ClaveSegura123!'
});
expect(response.user_id).toBeDefined();
expect(response.name).toBe('Juan Pérez');
expect(response.email).toBe('juan@ejemplo.com');
});
test('debe manejar peticiones concurrentes', async () => {
const userIds = ['1', '2', '3', '4', '5'];
const responses = await Promise.all(
userIds.map(id => getUser({ user_id: id }))
);
expect(responses).toHaveLength(5);
responses.forEach((response, index) => {
expect(response.user_id).toBe(userIds[index]);
});
});
});
Testing de Server Streaming
El server streaming permite al servidor enviar múltiples mensajes en respuesta a una única petición del cliente.
Testing de Streams de Servidor
test('debe transmitir lista de usuarios', (done) => {
const users = [];
const call = client.ListUsers({ limit: 100 });
call.on('data', (user) => {
users.push(user);
expect(user.user_id).toBeDefined();
expect(user.name).toBeDefined();
});
call.on('error', (error) => {
done(error);
});
call.on('end', () => {
expect(users.length).toBeGreaterThan(0);
expect(users.length).toBeLessThanOrEqual(100);
done();
});
});
Testing de Rendimiento de Streaming
test('debe transmitir usuarios eficientemente', (done) => {
const startTime = Date.now();
let messageCount = 0;
const call = client.ListUsers({ limit: 1000 });
call.on('data', (user) => {
messageCount++;
// Verificar que el streaming realmente está ocurriendo (no buffering)
const elapsed = Date.now() - startTime;
if (messageCount === 1) {
// El primer mensaje debe llegar rápidamente
expect(elapsed).toBeLessThan(100);
}
});
call.on('end', () => {
const totalTime = Date.now() - startTime;
const throughput = messageCount / (totalTime / 1000);
expect(messageCount).toBe(1000);
// Debe lograr al menos 100 mensajes por segundo
expect(throughput).toBeGreaterThan(100);
done();
});
call.on('error', done);
}, 30000);
Manejo de Errores en Streams
test('debe manejar errores de stream correctamente', (done) => {
const call = client.ListUsers({ limit: -1 }); // Límite inválido
call.on('data', () => {
fail('No debe recibir datos con parámetros inválidos');
});
call.on('error', (error) => {
expect(error.code).toBe(grpc.status.INVALID_ARGUMENT);
done();
});
});
Testing de Client Streaming
El client streaming permite al cliente enviar múltiples mensajes al servidor, que responde con un único mensaje.
Testing de Streams de Cliente
test('debe aceptar creación de usuarios por lotes', (done) => {
const call = client.CreateUsersBatch((error, response) => {
expect(error).toBeNull();
expect(response.created_count).toBe(3);
expect(response.user_ids).toHaveLength(3);
done();
});
// Enviar múltiples usuarios
call.write({ name: 'Usuario 1', email: 'usuario1@ejemplo.com' });
call.write({ name: 'Usuario 2', email: 'usuario2@ejemplo.com' });
call.write({ name: 'Usuario 3', email: 'usuario3@ejemplo.com' });
call.end();
});
Testing de Backpressure en Client Stream
test('debe manejar backpressure en client streaming', (done) => {
const call = client.CreateUsersBatch((error, response) => {
expect(error).toBeNull();
expect(response.created_count).toBe(10000);
done();
});
let writeCount = 0;
const totalWrites = 10000;
function writeNext() {
let canWrite = true;
while (canWrite && writeCount < totalWrites) {
writeCount++;
canWrite = call.write({
name: `Usuario ${writeCount}`,
email: `usuario${writeCount}@ejemplo.com`
});
if (!canWrite) {
// Esperar evento drain
call.once('drain', writeNext);
}
}
if (writeCount === totalWrites) {
call.end();
}
}
writeNext();
}, 30000);
Testing de Streaming Bidireccional
El streaming bidireccional permite tanto al cliente como al servidor enviar streams de mensajes independientemente.
Testing de Streams Bidireccionales
test('debe soportar chat con streaming bidireccional', (done) => {
const call = client.StreamUsers();
const receivedUsers = [];
call.on('data', (user) => {
receivedUsers.push(user);
});
call.on('end', () => {
expect(receivedUsers.length).toBeGreaterThan(0);
done();
});
call.on('error', done);
// Enviar peticiones como stream
call.write({ user_id: '1' });
call.write({ user_id: '2' });
call.write({ user_id: '3' });
setTimeout(() => {
call.end();
}, 1000);
}, 10000);
Testing de Comunicación en Tiempo Real
test('debe manejar comunicación bidireccional en tiempo real', (done) => {
const call = client.Chat();
const responses = [];
let messagesSent = 0;
call.on('data', (message) => {
responses.push(message);
// El servidor debe responder rápidamente
const responseTime = Date.now() - message.timestamp;
expect(responseTime).toBeLessThan(100);
if (responses.length === 5) {
call.end();
}
});
call.on('end', () => {
expect(responses).toHaveLength(5);
done();
});
call.on('error', done);
// Enviar mensajes con timestamps
const interval = setInterval(() => {
if (messagesSent < 5) {
call.write({
user_id: '123',
content: `Mensaje ${messagesSent + 1}`,
timestamp: Date.now()
});
messagesSent++;
} else {
clearInterval(interval);
}
}, 100);
}, 10000);
Testing de Interceptors
Los interceptors te permiten interceptar y modificar llamadas RPC, similar al middleware en servidores HTTP.
Testing de Client Interceptor
function createLoggingInterceptor() {
return function (options, nextCall) {
return new grpc.InterceptingCall(nextCall(options), {
start: function (metadata, listener, next) {
console.log('Iniciando llamada:', options.method_definition.path);
next(metadata, {
onReceiveMessage: function (message, next) {
console.log('Recibido:', message);
next(message);
}
});
}
});
};
}
test('debe aplicar interceptor de logging', (done) => {
const interceptedClient = new userProto.UserService(
'localhost:50051',
grpc.credentials.createInsecure(),
{ interceptors: [createLoggingInterceptor()] }
);
const consoleSpy = jest.spyOn(console, 'log');
interceptedClient.GetUser({ user_id: '123' }, (error, response) => {
expect(error).toBeNull();
expect(consoleSpy).toHaveBeenCalledWith(
'Iniciando llamada:',
expect.stringContaining('GetUser')
);
consoleSpy.mockRestore();
done();
});
});
Testing de Interceptor de Autenticación
function createAuthInterceptor(token) {
return function (options, nextCall) {
return new grpc.InterceptingCall(nextCall(options), {
start: function (metadata, listener, next) {
metadata.add('authorization', `Bearer ${token}`);
next(metadata, listener);
}
});
};
}
test('debe agregar token de autenticación vía interceptor', (done) => {
const authClient = new userProto.UserService(
'localhost:50051',
grpc.credentials.createInsecure(),
{ interceptors: [createAuthInterceptor('test-token-123')] }
);
authClient.GetUser({ user_id: '123' }, (error, response) => {
expect(error).toBeNull();
// Verificar que el servidor recibió el token de auth (necesitaría verificación del lado del servidor)
expect(response).toBeDefined();
done();
});
});
Manejo de Errores y Códigos de Estado
gRPC utiliza códigos de estado específicos para manejo de errores. El testing comprensivo debe cubrir todos los escenarios de error.
Testing de Códigos de Estado gRPC
const grpc = require('@grpc/grpc-js');
describe('Pruebas de Manejo de Errores', () => {
const errorScenarios = [
{
name: 'argumento inválido',
input: { user_id: '' },
expectedCode: grpc.status.INVALID_ARGUMENT
},
{
name: 'no encontrado',
input: { user_id: 'noexiste' },
expectedCode: grpc.status.NOT_FOUND
},
{
name: 'permiso denegado',
input: { user_id: 'restringido' },
expectedCode: grpc.status.PERMISSION_DENIED
},
{
name: 'deadline excedido',
input: { user_id: 'lento' },
expectedCode: grpc.status.DEADLINE_EXCEEDED,
deadline: Date.now() + 100 // timeout de 100ms
}
];
errorScenarios.forEach(scenario => {
test(`debe manejar ${scenario.name}`, (done) => {
const options = scenario.deadline
? { deadline: scenario.deadline }
: {};
client.GetUser(scenario.input, options, (error, response) => {
expect(error).toBeDefined();
expect(error.code).toBe(scenario.expectedCode);
done();
});
});
});
});
Testing de Metadata
test('debe enviar y recibir metadata', (done) => {
const metadata = new grpc.Metadata();
metadata.add('request-id', 'test-123');
metadata.add('client-version', '1.0.0');
const call = client.GetUser(
{ user_id: '123' },
metadata,
(error, response) => {
expect(error).toBeNull();
expect(response).toBeDefined();
}
);
call.on('metadata', (receivedMetadata) => {
// Verificar metadata de respuesta del servidor
const serverVersion = receivedMetadata.get('server-version');
expect(serverVersion).toBeDefined();
done();
});
});
Load Testing y Rendimiento
La eficiencia de gRPC lo hace ideal para escenarios de alto throughput. El load testing valida características de rendimiento.
Testing de Throughput con ghz
Usa ghz
para load testing de gRPC:
# Instalar ghz
go install github.com/bojand/ghz/cmd/ghz@latest
# Ejecutar load test
ghz --insecure \
--proto user.proto \
--call user.v1.UserService/GetUser \
-d '{"user_id":"123"}' \
-c 100 \
-n 10000 \
localhost:50051
Testing de Rendimiento Automatizado
const { spawn } = require('child_process');
test('debe manejar 10000 peticiones en menos de 10 segundos', async () => {
const ghz = spawn('ghz', [
'--insecure',
'--proto', 'user.proto',
'--call', 'user.v1.UserService/GetUser',
'-d', '{"user_id":"123"}',
'-c', '100',
'-n', '10000',
'--format', 'json',
'localhost:50051'
]);
let output = '';
ghz.stdout.on('data', (data) => {
output += data.toString();
});
await new Promise((resolve) => ghz.on('close', resolve));
const result = JSON.parse(output);
expect(result.average).toBeLessThan(10 * 1000000000); // 10 segundos en nanosegundos
expect(result.rps).toBeGreaterThan(1000); // Al menos 1000 req/s
expect(result.errorDistribution).toEqual({});
}, 30000);
Testing de Integración
Prueba servicios gRPC con dependencias reales y bases de datos.
Integración con TestContainers
const { GenericContainer } = require('testcontainers');
describe('Pruebas de Integración gRPC', () => {
let container;
let client;
beforeAll(async () => {
// Iniciar contenedor PostgreSQL
container = await new GenericContainer('postgres:14')
.withEnv('POSTGRES_PASSWORD', 'test')
.withExposedPorts(5432)
.start();
// Iniciar servidor gRPC con conexión DB
// ... inicializar client
}, 60000);
afterAll(async () => {
await container.stop();
});
test('debe persistir datos entre peticiones', async () => {
const createUser = promisify(client.CreateUser.bind(client));
const getUser = promisify(client.GetUser.bind(client));
const created = await createUser({
name: 'Usuario de Prueba de Integración',
email: 'integracion@ejemplo.com'
});
const retrieved = await getUser({ user_id: created.user_id });
expect(retrieved.name).toBe('Usuario de Prueba de Integración');
expect(retrieved.email).toBe('integracion@ejemplo.com');
});
});
Mejores Prácticas
Checklist de Testing
- ✅ Validar schemas proto con herramientas de linting
- ✅ Probar todos los tipos RPC: unario, server streaming, client streaming, bidireccional
- ✅ Verificar manejo de errores para todos los códigos de estado gRPC
- ✅ Probar interceptors para autenticación, logging y métricas
- ✅ Validar transmisión de metadata
- ✅ Probar comportamiento de deadline y timeout
- ✅ Realizar load testing para throughput esperado
- ✅ Probar manejo de backpressure en streams
- ✅ Verificar serialización/deserialización binaria
- ✅ Probar comunicación servicio-a-servicio
Tips de Optimización de Rendimiento
// Usar connection pooling
const channelOptions = {
'grpc.max_send_message_length': 100 * 1024 * 1024,
'grpc.max_receive_message_length': 100 * 1024 * 1024,
'grpc.keepalive_time_ms': 10000,
'grpc.keepalive_timeout_ms': 5000
};
const client = new userProto.UserService(
'localhost:50051',
grpc.credentials.createInsecure(),
channelOptions
);
Conclusión
Probar APIs gRPC requiere entender protocol buffers, patrones de streaming y características de HTTP/2. Al implementar pruebas comprensivas para todos los tipos RPC, validar schemas, probar interceptors y realizar load testing, aseguras que tus servicios gRPC sean confiables y eficientes.
Enfócate en el comportamiento de streaming, manejo de errores y características de rendimiento únicas de gRPC. Usa herramientas especializadas como ghz
para load testing y buf
para validación de schemas. Con estas prácticas, tus servicios gRPC estarán robustos y listos para producción.